Pārlūkot izejas kodu

feat: 制作新类型

bill 3 mēneši atpakaļ
vecāks
revīzija
19907ee947
34 mainītis faili ar 1282 papildinājumiem un 260 dzēšanām
  1. 1 1
      src/core/components/circle/index.ts
  2. 26 26
      src/core/components/group/group.vue
  3. 1 1
      src/core/components/icon/temp-icon.vue
  4. 7 2
      src/core/components/index.ts
  5. 79 23
      src/core/components/line/index.ts
  6. 6 77
      src/core/components/line/line.vue
  7. 158 0
      src/core/components/line/single-line.vue
  8. 102 57
      src/core/components/line/temp-line.vue
  9. 375 0
      src/core/components/line/use-draw.ts
  10. 1 1
      src/core/components/rectangle/index.ts
  11. 100 0
      src/core/components/sequent-line/index.ts
  12. 93 0
      src/core/components/sequent-line/line.vue
  13. 78 0
      src/core/components/sequent-line/temp-line.vue
  14. 13 3
      src/core/components/share/edit-line.vue
  15. 12 8
      src/core/components/share/edit-point.vue
  16. 3 3
      src/core/components/share/edit-polygon.vue
  17. 2 1
      src/core/components/share/size-line.vue
  18. 5 4
      src/core/helper/split-line.vue
  19. 39 19
      src/core/hook/use-draw.ts
  20. 4 3
      src/core/hook/use-expose.ts
  21. 5 5
      src/core/hook/use-mouse-status.ts
  22. 5 1
      src/core/hook/use-proportion.ts
  23. 2 2
      src/core/hook/use-status.ts
  24. 2 1
      src/core/hook/use-transformer.ts
  25. 4 3
      src/core/html-mount/propertys/components/num.vue
  26. 1 1
      src/core/html-mount/propertys/describes.json
  27. 1 0
      src/core/html-mount/propertys/mount.vue
  28. 3 2
      src/core/renderer/draw-group.vue
  29. 1 1
      src/core/store/store.ts
  30. 3 10
      src/example/components/slide/actions.ts
  31. 144 0
      src/example/fuse/enter.ts
  32. 1 0
      src/example/fuse/views/overview/index.vue
  33. 4 4
      src/example/platform/platform-draw.ts
  34. 1 1
      src/example/platform/platform-resource.ts

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

@@ -18,7 +18,7 @@ export const shapeName = "圆形";
 export const defaultStyle = {
   dash: [30, 0],
   stroke: '#000000',
-  strokeWidth: 2,
+  strokeWidth: 5,
   fontSize: 16,
   align: "center",
   fontStyle: "normal",

+ 26 - 26
src/core/components/group/group.vue

@@ -129,30 +129,30 @@ const operateMenus = shallowRef<
     handler: () => void;
   }>
 >([]);
-watchEffect(() => {
-  const item = store.getItemById(props.data.id);
-  if (item) {
-    operateMenus.value = [
-      {
-        label: `解除分组`,
-        handler() {
-          emit("delShape");
-        },
-      },
-    ];
-  } else {
-    operateMenus.value = [
-      {
-        label: `添加分组`,
-        handler() {
-          emit("addShape", {
-            ...props.data,
-            ...getBaseItem(),
-            stroke: undefined,
-          });
-        },
-      },
-    ];
-  }
-});
+// watchEffect(() => {
+//   const item = store.getItemById(props.data.id);
+//   if (item) {
+//     operateMenus.value = [
+//       {
+//         label: `解除分组`,
+//         handler() {
+//           emit("delShape");
+//         },
+//       },
+//     ];
+//   } else {
+//     operateMenus.value = [
+//       {
+//         label: `添加分组`,
+//         handler() {
+//           emit("addShape", {
+//             ...props.data,
+//             ...getBaseItem(),
+//             stroke: undefined,
+//           });
+//         },
+//       },
+//     ];
+//   }
+// });
 </script>

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

@@ -9,7 +9,7 @@
 
 <script lang="ts" setup>
 import { defaultStyle, IconData } from "./index.ts";
-import { computed, ref, watch, watchEffect } from "vue";
+import { computed, ref, watch } from "vue";
 import { getSvgContent, parseSvgContent, SVGParseResult } from "@/utils/resource.ts";
 import { Group } from "konva/lib/Group";
 import { DC } from "@/deconstruction.js";

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

@@ -10,7 +10,9 @@ import * as image from "./image";
 import * as table from "./table";
 import * as serial from "./serial";
 import * as group from "./group";
+import * as sequentLine from './sequent-line'
 
+import { SLineData } from "./sequent-line";
 import { ArrowData } from "./arrow";
 import { TableData } from "./table";
 import { RectangleData } from "./rectangle";
@@ -42,13 +44,15 @@ const _components = {
   image,
   table,
   serial,
-  group
+  group,
+  sequentLine
 };
 
 export const components = _components as Components
 
 export type Components = {
   [key in keyof typeof _components]: (typeof _components)[key] & {
+    useDraw?: () => void
     getSnapInfos?: (items: DrawItem<key>) => ComponentSnapInfo[];
     GroupComponent: (props: { data: DrawItem[] }) => any;
     getPredefine?: (attrKey: keyof DrawItem<key>) => any
@@ -71,7 +75,8 @@ export type DrawDataItem = {
   image: ImageData;
   table: TableData;
   serial: SerialData;
-  group: GroupData
+  group: GroupData,
+  sequentLine: SLineData
 };
 export type ShapeType = keyof DrawDataItem;
 

+ 79 - 23
src/core/components/line/index.ts

@@ -1,26 +1,28 @@
-import { Pos } from "@/utils/math.ts";
+import { lineVector, Pos, vectorAngle, verticalVector } from "@/utils/math.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Transform } from "konva/lib/Util";
-import { inRevise } from "@/utils/shared.ts";
+import { inRevise, onlyId, rangMod } from "@/utils/shared.ts";
+import { MathUtils } from "three";
+
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
+export { useDraw } from './use-draw.ts'
 
 export const shapeName = "线段";
 export const defaultStyle = {
-  stroke: '#000000',
-  strokeWidth: 5,
+  stroke: "#000000",
+  strokeWidth: 20,
   dash: [30, 0],
 };
 
-export const addMode = "dots";
+export const addMode = "single-dots";
 
 export const getMouseStyle = (data: LineData) => {
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
   const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
-
   return {
     default: { stroke: data.stroke || defaultStyle.stroke, strokeWidth },
     hover: { stroke: strokeStatus.hover },
@@ -30,8 +32,23 @@ export const getMouseStyle = (data: LineData) => {
   };
 };
 
-export const getSnapInfos = (data: LineData) =>
-  generateSnapInfos(getSnapPoints(data), true, true, true);
+export const getSnapInfos = (data: LineData) => {
+  const vh = generateSnapInfos(getSnapPoints(data), true, false, true);
+  data.lines.forEach(item => {
+    const a = data.points.find(p => p.id === item.a)!
+    const b = data.points.find(p => p.id === item.b)!
+    const prevVector = lineVector([a, b]);
+    const vLine = verticalVector(prevVector)
+
+    vh.push({
+      point: a,
+      links: [b],
+      linkDirections: [prevVector],
+      linkAngle: [rangMod(MathUtils.radToDeg(vectorAngle(vLine)), 180)],
+    });
+  })
+  return vh
+}
 
 export const getSnapPoints = (data: LineData) => {
   return data.points;
@@ -39,8 +56,19 @@ export const getSnapPoints = (data: LineData) => {
 
 export type LineData = Partial<typeof defaultStyle> &
   BaseItem & {
-    points: Pos[];
+    points: (Pos & { id: string })[];
+    lines: {
+      id: string;
+      a: string;
+      b: string;
+      strokeWidth: number;
+      stroke: string;
+      dash: number[];
+    }[];
+    polygon: { points: string[], id: string}[];
     attitude: number[];
+    updateTime?: number
+    calcTime?: number
   };
 
 export const interactiveToData: InteractiveTo<"line"> = ({
@@ -49,14 +77,17 @@ export const interactiveToData: InteractiveTo<"line"> = ({
   ...args
 }) => {
   if (info.cur) {
+    const baseItem = getBaseItem();
     return interactiveFixData({
       ...args,
       info,
       data: {
         ...defaultStyle,
-        ...getBaseItem(),
+        ...baseItem,
         ...preset,
+        lines: [],
         points: [],
+        polygon: [],
         attitude: [1, 0, 0, 1, 0, 0],
       },
     });
@@ -65,36 +96,61 @@ export const interactiveToData: InteractiveTo<"line"> = ({
 
 export const interactiveFixData: InteractiveFix<"line"> = ({ data, info }) => {
   const nv = [...info.consumed, info.cur!];
-  data.points.length = nv.length
+  data.points = []
+  data.lines = []
+  data.polygon = []
+
   for (let i = 0; i < nv.length; i++) {
     if (inRevise(data.points[i], nv[i])) {
-      data.points[i] = nv[i]
+      if (!data.points[i]) {
+        data.points[i] = {
+          id: onlyId(),
+          ...nv[i],
+        };
+      } else {
+        data.points[i] = {
+          ...data.points[i],
+          ...nv[i],
+        };
+      }
+    }
+  }
+  for (let i = 0; i < nv.length - 1; i++) {
+    if (!data.lines[i]) {
+      data.lines[i] = {
+        id: onlyId(),
+        ...defaultStyle,
+        a: data.points[i].id,
+        b: data.points[i + 1].id,
+      };
     }
   }
 
-  // data.points = [...info.consumed, info.cur!];
+  // data.polygon = [{points: [data.lines.map((item) => item.id)], id: onlyId()}];
   return data;
 };
 
-
-
-export const matResponse = ({data, mat, increment}: MatResponseProps<'line'>) => {
-  let transfrom: Transform
+export const matResponse = ({
+  data,
+  mat,
+  increment,
+}: MatResponseProps<"line">) => {
+  let transfrom: Transform;
   const attitude = new Transform(data.attitude);
   if (!increment) {
     const inverMat = attitude.copy().invert();
     transfrom = mat.copy().multiply(inverMat);
   } else {
-    transfrom = mat
+    transfrom = mat;
   }
 
-  data.points = data.points.map((v) => transfrom.point(v));
+  data.points = data.points.map((v) => ({ ...v, ...transfrom.point(v) }));
   data.attitude = transfrom.copy().multiply(attitude).m;
   return data;
-}
+};
 
 export const getPredefine = (key: keyof LineData) => {
-  if (key === 'strokeWidth') {
-    return { proportion: true }
+  if (key === "strokeWidth") {
+    return { proportion: true };
   }
-}
+};

+ 6 - 77
src/core/components/line/line.vue

@@ -1,93 +1,22 @@
 <template>
   <TempLine
-    :data="tData"
-    :ref="(v: any) => shape = v?.shape"
-    :id="data.id"
-    @update:position="updatePosition"
-    @update="emit('updateShape', { ...data })"
-    @deletePoint="deletePoint"
-    @addPoint="addPoint"
-    canEdit
-  />
-  <PropertyUpdate
-    :describes="describes"
     :data="data"
-    :target="shape"
-    @change="emit('updateShape', { ...data })"
-    @delete="emit('delShape')"
+    @updateShape="emit('updateShape', { ...data })"
+    :canEdit="true"
   />
-  <Operate :target="shape" :menus="operateMenus" />
+  
 </template>
 
 <script lang="ts" setup>
-import { LineData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
-import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
+import { useAutomaticData } from "@/core/hook/use-automatic-data.ts";
+import { LineData } from "./index.ts";
 import TempLine from "./temp-line.vue";
-import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
-import { useStore } from "@/core/store/index.ts";
-import { Pos } from "@/utils/math.ts";
 
 const props = defineProps<{ data: LineData }>();
+const data = useAutomaticData(() => props.data);
 const emit = defineEmits<{
   (e: "updateShape", value: LineData): void;
   (e: "addShape", value: LineData): void;
   (e: "delShape"): void;
 }>();
-
-const { shape, tData, data, operateMenus, describes } = useComponentStatus({
-  emit,
-  props,
-  getMouseStyle,
-  transformType: "line",
-  alignment(data, mat) {
-    return matResponse({ mat, data, increment: true });
-  },
-  // type: "line",
-  defaultStyle,
-  copyHandler(mat, data) {
-    return matResponse({ mat, data, increment: true });
-  },
-  propertys: [
-    "stroke",
-    "strokeWidth",
-    // "dash",
-    "opacity",
-    //  "ref", "zIndex"
-  ],
-});
-
-const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {
-  Object.assign(data.value.points[ndx], val);
-  shape.value?.getNode().fire("bound-change");
-};
-
-const deletePoint = (ndx: number) => {
-  data.value.points.splice(ndx, 1);
-  if (data.value.points.length <= 1) {
-    emit("delShape");
-  } else {
-    shape.value?.getNode().fire("bound-change");
-  }
-};
-
-const addPoint = ({ ndx, val }: { ndx: number; val: Pos }) => {
-  data.value.points.splice(ndx + 1, 0, val);
-  shape.value?.getNode().fire("bound-change");
-};
-
-const draw = useInteractiveDrawShapeAPI();
-const store = useStore();
-operateMenus.push({
-  label: "钢笔编辑",
-  handler() {
-    draw.enterDrawShape("line", {
-      ...props.data,
-      getMessages: () => {
-        const line = store.getItemById(props.data.id) as LineData;
-        return line ? line.points : [];
-      },
-    });
-  },
-});
 </script>

+ 158 - 0
src/core/components/line/single-line.vue

@@ -0,0 +1,158 @@
+<template>
+  <EditLine
+    :ref="(d: any) => shape = d?.shape"
+    :data="{ ...line, ...style }"
+    :opacity="addMode ? 0.3 : 1"
+    :points="points"
+    :closed="false"
+    :id="data.id"
+    :disablePoint="!canEdit || mode.include(Mode.readonly)"
+    :ndx="0"
+    @dragstart="emit('updateBefore')"
+    @update:line="
+      (p) => {
+        emit('updatePoint', { ...points[0], ...p[0] });
+        emit('updatePoint', { ...points[1], ...p[1] });
+      }
+    "
+    @dragend="emit('update')"
+    @add-point="addPoint"
+  />
+
+  <v-group>
+    <SizeLine
+      v-if="config.showComponentSize"
+      :points="points"
+      :strokeWidth="style.strokeWidth"
+      :stroke="style.stroke"
+    />
+  </v-group>
+
+  <template v-if="!mode.include(Mode.readonly) && canEdit">
+    <EditPoint
+      v-for="(point, ndx) in points"
+      :key="point.id"
+      :opacity="1"
+      :size="line.strokeWidth + 6"
+      :points="points"
+      :ndx="ndx"
+      :closed="false"
+      :id="line.id"
+      :disable="addMode"
+      :color="style.stroke"
+      @dragstart="dragstartHandler"
+      @update:position="(p) => emit('updatePoint', { ...point, ...p })"
+      @dragend="dragendHandler"
+      @delete="delPoint(point)"
+    />
+  </template>
+
+  <PropertyUpdate
+    :describes="describes"
+    :data="line"
+    :target="shape"
+    @change="
+      () => {
+        emit('updateBefore');
+        emit('updateLine', { ...line });
+        emit('update');
+      }
+    "
+    @delete="delHandler"
+  />
+  <Operate :target="shape" :menus="menus" />
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import { getMouseStyle, getSnapInfos, LineData } from "./index.ts";
+import { onlyId } from "@/utils/shared.ts";
+import EditLine from "../share/edit-line.vue";
+import { Pos } from "@/utils/math.ts";
+import EditPoint from "../share/edit-point.vue";
+import { Line } from "konva/lib/shapes/Line";
+import { DC } from "@/deconstruction.js";
+import { useMode } from "@/core/hook/use-status.ts";
+import { Mode } from "@/constant/mode.ts";
+import SizeLine from "../share/size-line.vue";
+import { useConfig } from "@/core/hook/use-config.ts";
+import { ComponentSnapInfo } from "../index.ts";
+import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
+import { mergeDescribes } from "@/core/html-mount/propertys/index.ts";
+import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
+import { useAnimationMouseStyle } from "@/core/hook/use-mouse-status.ts";
+
+const mode = useMode();
+
+const props = defineProps<{
+  line: LineData["lines"][number];
+  addMode?: boolean;
+  canEdit?: boolean;
+  data: LineData;
+}>();
+
+const emit = defineEmits<{
+  (e: "updatePoint", value: LineData["points"][number]): void;
+  (e: "addPoint", value: LineData["points"][number]): void;
+  (e: "delPoint", value: LineData["points"][number]): void;
+  (e: "delLine"): void;
+  (e: "updateLine", value: LineData["lines"][number]): void;
+  (e: "updateBefore"): void;
+  (e: "update"): void;
+}>();
+
+const shape = ref<DC<Line>>();
+const points = computed(() => [
+  props.data.points.find((p) => p.id === props.line.a)!,
+  props.data.points.find((p) => p.id === props.line.b)!,
+]);
+const lineData = computed(() => props.line);
+const describes = mergeDescribes(lineData, {}, ["stroke", "strokeWidth"]);
+const delHandler = () => {
+  emit("updateBefore");
+  emit("delLine");
+  emit("update");
+};
+const menus = [
+  {
+    label: "删除",
+    handler: delHandler,
+  },
+];
+
+const [style] = useAnimationMouseStyle({
+  shape,
+  getMouseStyle,
+  data: lineData as any,
+});
+
+const addPoint = (pos: Pos) => {
+  emit("updateBefore");
+  emit("addPoint", { ...points.value[0], ...pos, id: onlyId() });
+  emit("update");
+};
+
+const config = useConfig();
+const delPoint = (point: LineData["points"][number]) => {
+  emit("updateBefore");
+  emit("delPoint", point);
+  emit("update");
+};
+
+const infos = useCustomSnapInfos();
+let snapInfos: ComponentSnapInfo[];
+const dragstartHandler = () => {
+  emit("updateBefore");
+  snapInfos = getSnapInfos({
+    ...props.data,
+    lines: props.data.lines.filter((item) => item.id !== props.line.id),
+  });
+  snapInfos.forEach((item) => {
+    infos.add(item);
+  });
+};
+const dragendHandler = () => {
+  emit("update");
+  snapInfos.forEach((item) => infos.remove(item));
+};
+</script>

+ 102 - 57
src/core/components/line/temp-line.vue

@@ -1,78 +1,123 @@
 <template>
-  <v-group ref="shape">
-    <v-line
-      name="repShape"
-      :config="{
-        ...data,
-        zIndex: undefined,
-        points: flatPositions(data.points),
-        opacity: addMode ? 0.3 : data.opacity,
-        hitFunc,
-      }"
+  <v-group :id="data.id" ref="shape">
+    <singleLine
+      v-for="item in data.lines"
+      :key="item.id"
+      :line="item"
+      :data="data"
+      :add-mode="addMode"
+      :can-edit="!initData"
+      @add-point="(p) => addPointHandler(p, item)"
+      @del-point="delPointHandler"
+      @update-point="updatePointHandler"
+      @update-before="updateBeforeHandler"
+      @update="updateHandler"
+      @update-line="updateLineHandler"
+      @del-line="delLineHandler(item)"
     />
-    <v-group>
-      <SizeLine
-        v-if="config.showComponentSize"
-        :points="data.points"
-        :strokeWidth="data.strokeWidth"
-        :stroke="data.stroke"
-      />
-    </v-group>
-    <v-group>
-      <EditPolygon
-        :data="data"
-        :shape="shape"
-        :addMode="addMode"
-        :canEdit="canEdit"
-        @update:position="(data) => emit('update:position', data)"
-        @update="emit('update')"
-        @deletePoint="(ndx) => emit('deletePoint', ndx)"
-        @addPoint="(data) => emit('addPoint', data)"
-        v-if="shape"
-      />
-    </v-group>
   </v-group>
 </template>
 
 <script lang="ts" setup>
-import EditPolygon from "../share/edit-polygon.vue";
-import SizeLine from "../share/size-line.vue";
-import { defaultStyle, LineData } from "./index.ts";
-import { flatPositions } from "@/utils/shared.ts";
+import { onlyId } from "@/utils/shared.ts";
+import { LineData } from "./index.ts";
+import singleLine from "./single-line.vue";
+import { getInitCtx, NLineDataCtx, normalLineData, useInitData } from "./use-draw.ts";
 import { computed, ref } from "vue";
+import { useZIndex } from "@/core/hook/use-layer.ts";
 import { DC } from "@/deconstruction.js";
-import { Line, LineConfig } from "konva/lib/shapes/Line";
-import { Pos } from "@/utils/math.ts";
-import { useConfig } from "@/core/hook/use-config.ts";
+import { Group } from "konva/lib/Group";
 
 const props = defineProps<{
   data: LineData;
   addMode?: boolean;
   canEdit?: boolean;
 }>();
+const initData = useInitData();
 const emit = defineEmits<{
-  (e: "update:position", data: { ndx: number; val: Pos }): void;
-  (e: "addPoint", data: { ndx: number; val: Pos }): void;
-  (e: "update"): void;
-  (e: "deletePoint", ndx: number): void;
+  (e: "updateShape"): void;
 }>();
 
-const data = computed(() => ({ ...defaultStyle, ...props.data }));
-const config = useConfig();
-const hitFunc: LineConfig["hitFunc"] = (con, shape) => {
-  con.beginPath();
-  con.moveTo(data.value.points[0].x, data.value.points[0].y);
-  for (let i = 1; i < data.value.points.length; i++) {
-    con.lineTo(data.value.points[i].x, data.value.points[i].y);
+const data = computed(() => {
+  if (!initData.value || props.addMode) return props.data;
+  return initData.value;
+});
+
+let track = false;
+let ctx: NLineDataCtx;
+const updateBeforeHandler = () => {
+  track = true;
+  ctx = getInitCtx();
+  console.log("??");
+};
+
+const delPointHandler = (p: LineData["points"][0]) => {
+  const checkLines = props.data.lines.filter(
+    (item) => item.a === p.id || item.b === p.id
+  );
+  if (checkLines.length > 1) {
+    const joinPoints = new Set<string>();
+    checkLines.forEach((item) => {
+      joinPoints.add(item.a);
+      joinPoints.add(item.b);
+    });
+
+    if (joinPoints.size === 3) {
+      const prev = checkLines.find((item) => item.b === p.id);
+      const next = checkLines.find((item) => item.a === p.id);
+      if (prev && next) {
+        addLineHandler({ ...prev, id: onlyId(), b: next.b });
+      } else {
+        const l = prev || next || checkLines[0];
+        const ps = [...joinPoints].filter((item) => item !== p.id);
+        addLineHandler({ ...l, id: onlyId(), a: ps[0], b: ps[1] });
+      }
+    }
   }
-  con.closePath();
-  con.fillStrokeShape(shape);
+  checkLines.forEach(delLineHandler);
+  ctx.del.points[p.id] = p;
+  const ndx = props.data.points.findIndex((pn) => pn.id === p.id);
+  ~ndx && props.data.points.splice(ndx, 1);
 };
 
-const shape = ref<DC<Line>>();
-defineExpose({
-  get shape() {
-    return shape.value;
-  },
-});
+const addPointHandler = (p: LineData["points"][0], l: LineData["lines"][0]) => {
+  props.data.points.push(p);
+  ctx.add.points[p.id] = p;
+  delLineHandler(l);
+  addLineHandler({ ...l, a: p.id, id: onlyId() });
+  addLineHandler({ ...l, b: p.id, id: onlyId() });
+};
+
+const updatePointHandler = (p: LineData["points"][0]) => {
+  const ndx = props.data.points.findIndex((pn) => pn.id === p.id);
+  ctx.update.points[p.id] = p;
+  props.data.points[ndx] = p;
+};
+const delLineHandler = (l: LineData["lines"][0]) => {
+  ctx.del.lines[l.id] = l;
+  const ndx = props.data.lines.findIndex((ln) => ln.id === l.id);
+  ~ndx && props.data.lines.splice(ndx, 1);
+};
+const addLineHandler = (l: LineData["lines"][0]) => {
+  ctx.add.lines[l.id] = l;
+  props.data.lines.push(l);
+};
+
+const updateLineHandler = (l: LineData["lines"][0]) => {
+  console.log(l);
+  // const ndx = props.data.lines.findIndex((ln) => ln.id === l.id);
+  // props.data.lines[ndx] = l;
+};
+
+const updateHandler = () => {
+  normalLineData(props.data, ctx);
+  emit("updateShape");
+  track = false;
+};
+
+const shape = ref<DC<Group>>();
+useZIndex(
+  shape,
+  computed(() => props.data)
+);
 </script>

+ 375 - 0
src/core/components/line/use-draw.ts

@@ -0,0 +1,375 @@
+import {
+  MessageAction,
+  penUpdatePoints,
+  useDrawRunning,
+  useInteractiveDrawShapeAPI,
+  usePointBeforeHandler,
+} from "@/core/hook/use-draw";
+import { components, ComponentSnapInfo, SnapPoint } from "..";
+import { useHistory, useHistoryAttach } from "@/core/hook/use-history";
+import { useStore } from "@/core/store";
+import { useViewerTransform } from "@/core/hook/use-viewer";
+import { useOperMode } from "@/core/hook/use-status";
+import { installGlobalVar, useCursor } from "@/core/hook/use-global-vars";
+import { useInteractiveDots } from "@/core/hook/use-interactive";
+import { computed, reactive, ref, watch } from "vue";
+import { copy, mergeFuns } from "@/utils/shared";
+import { eqPoint, Pos } from "@/utils/math";
+import { getSnapInfos, type LineData } from "./";
+import { useCustomSnapInfos } from "@/core/hook/use-snap";
+
+type PayData = Pos;
+
+export let initData: LineData | undefined;
+export const useInitData = installGlobalVar(() => ref<LineData>());
+
+// 单例钢笔添加
+export const useDraw = () => {
+  const type = "line";
+  const { quitDrawShape } = useInteractiveDrawShapeAPI();
+  const isRuning = useDrawRunning(type);
+  const obj = components[type];
+  const beforeHandler = usePointBeforeHandler(true, true);
+  const history = useHistory();
+  const store = useStore();
+  const viewTransform = useViewerTransform();
+  const operMode = useOperMode();
+  const hInitData = useInitData();
+  const customSnapInfos = useCustomSnapInfos();
+
+  // 可能历史空间会撤销 重做更改到正在绘制的组件
+  const currentCursor = ref("/icons/m_add.png");
+  const cursor = useCursor();
+  let cursorPop: ReturnType<typeof cursor.push> | null = null;
+  let stopWatch: (() => void) | null = null;
+  let prev: SnapPoint;
+  let currentIsDel = false;
+  let isTempDraw = false;
+  let snapInfos: ComponentSnapInfo[] | null = null;
+  let drawSnapInfos: ComponentSnapInfo[] | null = null;
+
+  const ia = useInteractiveDots({
+    shapeType: type,
+    isRuning,
+    enter() {
+      cursorPop = cursor.push(currentCursor.value);
+      watch(currentCursor, () => {
+        cursorPop?.set(currentCursor.value);
+      });
+    },
+    quit: () => {
+      quitDrawShape();
+      beforeHandler.clear();
+      cursorPop && cursorPop();
+      stopWatch && stopWatch();
+      if (!drawItems[0]) return;
+
+      if (isTempDraw) {
+        drawItems[0].lines.pop();
+        drawItems[0].points.pop();
+        drawItems[0].polygon.pop();
+      }
+      snapInfos?.forEach(customSnapInfos.remove);
+      drawSnapInfos?.forEach(customSnapInfos.remove);
+      initData = hInitData.value = void 0;
+    },
+    beforeHandler: (p) => {
+      beforeHandler.clear();
+      const pa = beforeHandler.transform(p, prev && [prev, p]);
+      currentIsDel && beforeHandler.clear();
+      return pa;
+    },
+  });
+
+  const shapeId = computed(
+    () => ia.isRunning.value && store.getTypeItems(type)[0]?.id
+  );
+  const drawItems = reactive([]) as LineData[];
+  watch(
+    shapeId,
+    () => {
+      const data = shapeId.value
+        ? (store.getItemById(shapeId.value) as LineData)
+        : undefined;
+
+      if (data) {
+        initData = hInitData.value = {
+          ...data,
+          points: [...data.points],
+          lines: [...data.lines],
+          polygon: [...data.polygon],
+        };
+        drawItems[0] = {
+          ...data,
+          points: [],
+          lines: [],
+          polygon: [],
+        };
+        snapInfos = getSnapInfos(initData);
+        snapInfos.forEach(customSnapInfos.add);
+      } else {
+        drawItems.pop();
+        initData = hInitData.value = undefined;
+      }
+    },
+    { immediate: true }
+  );
+
+  const messages = useHistoryAttach<Pos[]>(
+    `${type}-pen`,
+    isRuning,
+    () => [],
+    true
+  );
+
+  const getAddMessage = (cur: Pos) => {
+    let consumed = messages.value;
+    currentCursor.value = "/icons/m_add.png";
+    let pen: null | ReturnType<typeof penUpdatePoints> = null;
+    return {
+      pen,
+      consumed,
+      cur,
+      action: MessageAction.add,
+    } as any;
+  };
+
+  const setMessage = (cur: Pos) => {
+    const { pen, ...msg } = getAddMessage(cur);
+    if ((currentIsDel = pen?.oper === "del")) {
+      currentCursor.value = "/icons/m_reduce.png";
+      beforeHandler.clear();
+    }
+    return msg;
+  };
+
+  const pushMessages = (cur: Pos) => {
+    const { pen } = getAddMessage(cur);
+    if (pen) {
+      if (!pen.unchanged) {
+        messages.value = pen.points;
+        cur = pen.cur;
+        messages.value.push(cur);
+      }
+    } else {
+      messages.value.push(cur);
+    }
+    return !pen?.unchanged;
+  };
+
+  const addItem = (cur: PayData) => {
+    if (!drawItems[0]) {
+      const data = obj.interactiveToData({
+        preset: ia.preset as any,
+        info: setMessage(cur),
+        viewTransform: viewTransform.value,
+        history,
+        store,
+      });
+      if (!data) {
+        drawItems.pop();
+        return;
+      }
+      if (initData?.id) {
+        data.id = initData?.id;
+      }
+      drawItems[0] = reactive(data);
+    }
+
+    let prevItemIds: string[] = [];
+    const storeAddItem = (cItem: LineData) => {
+      drawSnapInfos?.forEach(customSnapInfos.remove);
+      drawSnapInfos = getSnapInfos(cItem);
+      const ctx = getInitCtx();
+      cItem.points.forEach((p) => {
+        if (!prevItemIds.includes(p.id)) {
+          prevItemIds.push(p.id);
+          ctx.add.points[p.id] = p;
+        }
+      });
+      cItem.lines.forEach((l) => {
+        if (!prevItemIds.includes(l.id)) {
+          prevItemIds.push(l.id);
+          ctx.add.lines[l.id] = l;
+        }
+      });
+
+      if (initData) {
+        cItem = {
+          ...cItem,
+          points: [...initData.points, ...cItem.points],
+          lines: [...initData.lines, ...cItem.lines],
+          polygon: [...initData.polygon, ...cItem.polygon],
+        };
+      } else {
+        cItem = {
+          ...cItem,
+          points: [...cItem.points],
+          lines: [...cItem.lines],
+          polygon: [...cItem.polygon],
+        };
+      }
+      cItem = normalLineData(cItem, ctx);
+      console.log(cItem);
+      drawSnapInfos.forEach(customSnapInfos.add);
+      if (drawItems[0] && store.getItemById(drawItems[0].id)) {
+        store.setItem(type, { id: cItem.id, value: cItem });
+      } else {
+        store.addItem(type, cItem);
+      }
+    };
+
+    if (ia.singleDone.value) {
+      storeAddItem(drawItems[0]);
+      return;
+    }
+
+    const update = () => {
+      const msg = setMessage(cur);
+      drawItems[0] = obj.interactiveFixData({
+        data: drawItems[0]!,
+        info: msg,
+        viewTransform: viewTransform.value,
+        history,
+        store,
+      });
+      isTempDraw = true;
+    };
+
+    stopWatch = mergeFuns(
+      watch(() => operMode.value.freeDraw, update),
+      watch(cur, update, { immediate: true, deep: true }),
+      watch(
+        messages,
+        () => {
+          if (!messages.value) return;
+          if (messages.value.length === 0) {
+            quitDrawShape();
+          } else {
+            update();
+          }
+        },
+        { deep: true }
+      ),
+      // 监听是否消费完毕
+      watch(ia.singleDone, () => {
+        prev = { ...cur, view: true };
+        const isChange = pushMessages(cur);
+        if (isChange) {
+          storeAddItem(copy(drawItems[0]));
+        }
+        beforeHandler.clear();
+        stopWatch && stopWatch();
+        stopWatch = null;
+        isTempDraw = false;
+      })
+    );
+  };
+
+  // 每次拽结束都加组件
+  watch(
+    () => ia.messages,
+    (datas: any) => {
+      datas.forEach(addItem);
+      ia.consume(datas);
+    },
+    { immediate: true }
+  );
+
+  return drawItems;
+};
+
+export type NLineDataCtx = {
+  del: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineData["lines"][0]>;
+  };
+  add: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineData["lines"][0]>;
+  };
+  update: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineData["lines"][0]>;
+  };
+};
+export const getInitCtx = (): NLineDataCtx => ({
+  del: {
+    points: {},
+    lines: {},
+  },
+  add: {
+    points: {},
+    lines: {},
+  },
+  update: {
+    points: {},
+    lines: {},
+  },
+});
+
+export const repPointRef = (data: LineData, delId: string, repId: string) => {
+  for (let i = 0; i < data.lines.length; i++) {
+    const line = data.lines[i];
+    if (line.a === delId) {
+      data.lines[i] = { ...line, a: repId };
+    }
+    if (line.b === delId) {
+      data.lines[i] = { ...line, b: repId };
+    }
+  }
+  return data
+};
+
+export const deduplicateLines = (data: LineData) => {
+  const seen = new Map<string, LineData['lines'][0]>();
+  let isChange = false
+  for (const line of data.lines) {
+    if (line.a === line.b) continue;
+    // 生成标准化键:确保 (a,b) 和 (b,a) 被视为相同,并且 a === b 时也去重
+    const key1 = `${line.a},${line.b}`;
+    const key2 = `${line.b},${line.a}`;
+
+    // 检查是否已存在相同键
+    const existingKey = seen.has(key1) ? key1 : seen.has(key2) ? key2 : null;
+
+    if (existingKey) {
+      // 如果存在重复键,覆盖旧值(保留尾部元素)
+      seen.delete(existingKey);
+      seen.set(key1, line); // 统一存储为 key1 格式
+      isChange = true
+    } else {
+      // 新记录,直接存储
+      seen.set(key1, line);
+    }
+  }
+  if (isChange) {
+    data.lines = Array.from(seen.values())
+  }
+
+  return data
+};
+
+export const normalLineData = (data: LineData, ctx: NLineDataCtx) => {
+  const changePoints = [
+    ...Object.values(ctx.add.points),
+    ...Object.values(ctx.update.points),
+  ];
+
+  for (const p2 of changePoints) {
+    const ndx = data.points.findIndex((item) => item.id === p2.id);
+    if (!~ndx) continue;
+
+    for (let i = 0; i < data.points.length; i++) {
+      const p1 = data.points[i];
+      if (p1.id !== p2.id && eqPoint(p1, p2)) {
+        repPointRef(data, p1.id, p2.id);
+        data.points.splice(i, 1);
+        i--;
+        console.log("p1 pre", p1, p2);
+      }
+    }
+  }
+
+  return deduplicateLines(data);
+};

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

@@ -11,7 +11,7 @@ export { default as TempComponent } from "./temp-rectangle.vue";
 export const shapeName = "矩形";
 export const defaultStyle = {
   dash: [30, 0],
-  strokeWidth: 1,
+  strokeWidth: 5,
   stroke: "#000000",
   fontSize: 16,
   align: "center",

+ 100 - 0
src/core/components/sequent-line/index.ts

@@ -0,0 +1,100 @@
+import { Pos } from "@/utils/math.ts";
+import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
+import { getMouseColors } from "@/utils/colors.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
+import { inRevise } from "@/utils/shared.ts";
+
+export { default as Component } from "./line.vue";
+export { default as TempComponent } from "./temp-line.vue";
+
+export const shapeName = "连续线段";
+export const defaultStyle = {
+  stroke: '#000000',
+  strokeWidth: 20,
+  dash: [30, 0],
+};
+
+export const addMode = "dots";
+
+export const getMouseStyle = (data: SLineData) => {
+  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
+
+  return {
+    default: { stroke: data.stroke || defaultStyle.stroke, strokeWidth },
+    hover: { stroke: strokeStatus.hover },
+    select: { stroke: strokeStatus.select },
+    focus: { stroke: strokeStatus.hover },
+    press: { stroke: strokeStatus.press },
+  };
+};
+
+export const getSnapInfos = (data: SLineData) =>
+  generateSnapInfos(getSnapPoints(data), true, true, true);
+
+export const getSnapPoints = (data: SLineData) => {
+  return data.points;
+};
+
+export type SLineData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    points: Pos[];
+    attitude: number[];
+  };
+
+export const interactiveToData: InteractiveTo<'sequentLine'> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
+  if (info.cur) {
+    return interactiveFixData({
+      ...args,
+      info,
+      data: {
+        ...defaultStyle,
+        ...getBaseItem(),
+        ...preset,
+        points: [],
+        attitude: [1, 0, 0, 1, 0, 0],
+      },
+    });
+  }
+};
+
+export const interactiveFixData: InteractiveFix<'sequentLine'> = ({ data, info }) => {
+  const nv = [...info.consumed, info.cur!];
+  data.points.length = nv.length
+  for (let i = 0; i < nv.length; i++) {
+    if (inRevise(data.points[i], nv[i])) {
+      data.points[i] = nv[i]
+    }
+  }
+
+  // data.points = [...info.consumed, info.cur!];
+  return data;
+};
+
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'sequentLine'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}
+
+export const getPredefine = (key: keyof SLineData) => {
+  if (key === 'strokeWidth') {
+    return { proportion: true }
+  }
+}

+ 93 - 0
src/core/components/sequent-line/line.vue

@@ -0,0 +1,93 @@
+<template>
+  <TempLine
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @update:position="updatePosition"
+    @update="emit('updateShape', { ...data })"
+    @deletePoint="deletePoint"
+    @addPoint="addPoint"
+    canEdit
+  />
+  <PropertyUpdate
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @change="emit('updateShape', { ...data })"
+    @delete="emit('delShape')"
+  />
+  <Operate :target="shape" :menus="operateMenus" />
+</template>
+
+<script lang="ts" setup>
+import { SLineData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
+import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
+import TempLine from "./temp-line.vue";
+import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
+import { useStore } from "@/core/store/index.ts";
+import { Pos } from "@/utils/math.ts";
+
+const props = defineProps<{ data: SLineData }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: SLineData): void;
+  (e: "addShape", value: SLineData): void;
+  (e: "delShape"): void;
+}>();
+
+const { shape, tData, data, operateMenus, describes } = useComponentStatus({
+  emit,
+  props,
+  getMouseStyle,
+  transformType: "line",
+  alignment(data, mat) {
+    return matResponse({ mat, data, increment: true });
+  },
+  // type: "line",
+  defaultStyle,
+  copyHandler(mat, data) {
+    return matResponse({ mat, data, increment: true });
+  },
+  propertys: [
+    "stroke",
+    "strokeWidth",
+    // "dash",
+    "opacity",
+    //  "ref", "zIndex"
+  ],
+});
+
+const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {
+  Object.assign(data.value.points[ndx], val);
+  shape.value?.getNode().fire("bound-change");
+};
+
+const deletePoint = (ndx: number) => {
+  data.value.points.splice(ndx, 1);
+  if (data.value.points.length <= 1) {
+    emit("delShape");
+  } else {
+    shape.value?.getNode().fire("bound-change");
+  }
+};
+
+const addPoint = ({ ndx, val }: { ndx: number; val: Pos }) => {
+  data.value.points.splice(ndx + 1, 0, val);
+  shape.value?.getNode().fire("bound-change");
+};
+
+const draw = useInteractiveDrawShapeAPI();
+const store = useStore();
+operateMenus.push({
+  label: "钢笔编辑",
+  handler() {
+    draw.enterDrawShape("sequentLine", {
+      ...props.data,
+      getMessages: () => {
+        const line = store.getItemById(props.data.id) as SLineData;
+        return line ? line.points : [];
+      },
+    });
+  },
+});
+</script>

+ 78 - 0
src/core/components/sequent-line/temp-line.vue

@@ -0,0 +1,78 @@
+<template>
+  <v-group ref="shape">
+    <v-line
+      name="repShape"
+      :config="{
+        ...data,
+        zIndex: undefined,
+        points: flatPositions(data.points),
+        opacity: addMode ? 0.3 : data.opacity,
+        hitFunc,
+      }"
+    />
+    <v-group>
+      <SizeLine
+        v-if="config.showComponentSize"
+        :points="data.points"
+        :strokeWidth="data.strokeWidth"
+        :stroke="data.stroke"
+      />
+    </v-group>
+    <v-group>
+      <EditPolygon
+        :data="data"
+        :shape="shape"
+        :addMode="addMode"
+        :canEdit="canEdit"
+        @update:position="(data) => emit('update:position', data)"
+        @update="emit('update')"
+        @deletePoint="(ndx) => emit('deletePoint', ndx)"
+        @addPoint="(data) => emit('addPoint', data)"
+        v-if="shape"
+      />
+    </v-group>
+  </v-group>
+</template>
+
+<script lang="ts" setup>
+import EditPolygon from "../share/edit-polygon.vue";
+import SizeLine from "../share/size-line.vue";
+import { defaultStyle, SLineData } from "./index.ts";
+import { flatPositions } from "@/utils/shared.ts";
+import { computed, ref } from "vue";
+import { DC } from "@/deconstruction.js";
+import { Line, LineConfig } from "konva/lib/shapes/Line";
+import { Pos } from "@/utils/math.ts";
+import { useConfig } from "@/core/hook/use-config.ts";
+
+const props = defineProps<{
+  data: SLineData;
+  addMode?: boolean;
+  canEdit?: boolean;
+}>();
+const emit = defineEmits<{
+  (e: "update:position", data: { ndx: number; val: Pos }): void;
+  (e: "addPoint", data: { ndx: number; val: Pos }): void;
+  (e: "update"): void;
+  (e: "deletePoint", ndx: number): void;
+}>();
+
+const data = computed(() => ({ ...defaultStyle, ...props.data }));
+const config = useConfig();
+const hitFunc: LineConfig["hitFunc"] = (con, shape) => {
+  con.beginPath();
+  con.moveTo(data.value.points[0].x, data.value.points[0].y);
+  for (let i = 1; i < data.value.points.length; i++) {
+    con.lineTo(data.value.points[i].x, data.value.points[i].y);
+  }
+  con.closePath();
+  con.fillStrokeShape(shape);
+};
+
+const shape = ref<DC<Line>>();
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+});
+</script>

+ 13 - 3
src/core/components/share/edit-line.vue

@@ -3,13 +3,15 @@
     ref="line"
     :config="{
       strokeWidth: data.strokeWidth,
-      opacity: 0,
+      opacity: opacity || 0,
+      stroke: data.stroke,
       points: flatPositions(points),
       hitStrokeWidth: data.strokeWidth + 20,
     }"
   />
 
   <v-circle
+    v-if="!disablePoint"
     :config="{ ...pointStyle, ...center, opacity: isHover || isPointHover ? 1 : 0 }"
     ref="point"
   />
@@ -17,7 +19,6 @@
 
 <script lang="ts" setup>
 import { copy, flatPositions } from "@/utils/shared";
-import { LineData } from "../line";
 import { computed, ref, watch } from "vue";
 import { DC } from "@/deconstruction";
 import { Line } from "konva/lib/shapes/Line";
@@ -31,14 +32,17 @@ import { generateSnapInfos } from "../util";
 import { getMouseColors } from "@/utils/colors";
 import { themeColor } from "@/constant/help-style";
 import { Circle } from "konva/lib/shapes/Circle";
+import { SLineData } from "../sequent-line";
 
-type LData = Required<Pick<LineData, "strokeWidth" | "stroke">>;
+type LData = Required<Pick<SLineData, "strokeWidth" | "stroke">>;
 const props = defineProps<{
   data: LData;
   points: Pos[];
   id: string;
   ndx: number;
   closed?: boolean;
+  disablePoint?: boolean;
+  opacity?: number;
 }>();
 const emit = defineEmits<{
   (e: "update:line", data: Pos[]): void;
@@ -143,4 +147,10 @@ const pointStyle = computed(() => {
     stroke: color.pub,
   };
 });
+
+defineExpose({
+  get shape() {
+    return line.value;
+  },
+});
 </script>

+ 12 - 8
src/core/components/share/edit-point.vue

@@ -3,6 +3,7 @@
     :config="{ ...style, ...position, hitStrokeWidth: style.strokeWidth + 10 }"
     ref="circle"
   />
+  <Operate :target="circle" :menus="[{ label: '删除', handler: () => emit('delete') }]" />
 </template>
 
 <script lang="ts" setup>
@@ -16,9 +17,10 @@ import { getMouseColors } from "@/utils/colors";
 import { useCustomSnapInfos, useGlobalSnapInfos, useSnap } from "@/core/hook/use-snap";
 import { generateSnapInfos } from "../util";
 import { ComponentSnapInfo } from "..";
-import { useShapeClick, useShapeIsHover } from "@/core/hook/use-mouse-status";
+import { useShapeIsHover } from "@/core/hook/use-mouse-status";
 import { useCursor } from "@/core/hook/use-global-vars";
 import { rangMod } from "@/utils/shared";
+import { Operate } from "../../html-mount/propertys/index.ts";
 
 const props = defineProps<{
   points: Pos[];
@@ -27,6 +29,7 @@ const props = defineProps<{
   color?: string;
   size?: number;
   disable?: boolean;
+  opacity?: number;
   closed?: boolean;
   notDelete?: boolean;
   getSelfSnapInfos?: (point: Pos) => ComponentSnapInfo[];
@@ -48,7 +51,7 @@ const style = computed(() => {
     fill: dragIng.value ? "#fff" : color.pub,
     stroke: color.pub,
     strokeWidth: size / 4,
-    opacity: props.disable ? 0.5 : 1,
+    opacity: props.opacity !== undefined ? props.opacity : props.disable ? 0.5 : 1,
   };
 });
 
@@ -98,16 +101,11 @@ if (!props.notDelete) {
     isHover,
     (hover, _, onCleanup) => {
       if (hover) {
-        onCleanup(cursor.push("/icons/m_reduce.png"));
+        onCleanup(cursor.push("/icons/m_move.png"));
       }
     },
     { immediate: true }
   );
-
-  useShapeClick(circle, () => {
-    emit("delete");
-    isHover.value = false;
-  });
 }
 const dragIng = ref(false);
 let init: Pos;
@@ -151,4 +149,10 @@ watch(
   },
   { immediate: true }
 );
+
+defineExpose({
+  get shape() {
+    return circle.value;
+  },
+});
 </script>

+ 3 - 3
src/core/components/share/edit-polygon.vue

@@ -1,7 +1,7 @@
 <template>
   <v-group>
     <template
-      v-if="status.hover && canEdit && !operMode.mulSelection && data.points.length > 2"
+      v-if="status.hover && canEdit && !operMode.mulSelection && data.points.length >= 2"
     >
       <EditLine
         :data="data"
@@ -53,7 +53,7 @@
 <script lang="ts" setup>
 import Point from "../share/edit-point.vue";
 import EditLine from "../share/edit-line.vue";
-import { LineData } from "../line";
+import { SLineData } from "../sequent-line";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { Pos } from "@/utils/math.ts";
 import { useMouseShapeStatus } from "@/core/hook/use-mouse-status";
@@ -61,7 +61,7 @@ import { computed } from "vue";
 import { useOperMode } from "@/core/hook/use-status";
 
 const props = defineProps<{
-  data: Required<Pick<LineData, "id" | "points" | "stroke" | "strokeWidth">>;
+  data: Required<Pick<SLineData, "id" | "points" | "stroke" | "strokeWidth">>;
   addMode?: boolean;
   canEdit?: boolean;
   closed?: boolean;

+ 2 - 1
src/core/components/share/size-line.vue

@@ -61,6 +61,7 @@ const getLine = (ndx: number) =>
 const isClockwise = computed(() => getPolygonDirection(props.points) <= 0);
 
 const proportion = useProportion();
+const getWidthText = (val: number) => Math.floor(proportion.transform(val)).toString();
 const margin = computed(() =>
   props.margin !== undefined ? props.margin! : -10 - style.value.strokeWidth * 2
 );
@@ -71,7 +72,7 @@ const lines = computed(() => {
     const baseLineVV = lineVerticalVector(baseLine);
     const sizeMargin = isClockwise.value ? margin.value : -margin.value;
     const sizeOffset = baseLineVV.clone().multiplyScalar(sizeMargin);
-    const sizeTextStr = proportion.transform(lineLen(baseLine[0], baseLine[1]));
+    const sizeTextStr = getWidthText(lineLen(baseLine[0], baseLine[1]));
     const sizeTextW = (fontSize.value * sizeTextStr.length) / 1.5;
     const baseLineW = lineLen(baseLine[0], baseLine[1]);
 

+ 5 - 4
src/core/helper/split-line.vue

@@ -152,7 +152,8 @@ const rectAxisSteps = computed(() => {
   return axis;
 });
 
-const { transform: getWidthText } = useProportion();
+const { transform } = useProportion();
+const getWidthText = (val: number) => Math.floor(transform(val)).toString();
 const invConfig = useViewerInvertTransformConfig();
 const axissInfo = computed(() => {
   if (!rang.value) return;
@@ -217,7 +218,7 @@ const axissInfo = computed(() => {
 
       infos.right.texts.push({
         width: width,
-        text: getWidthText(width * invConfig.value.scaleY),
+        text: getWidthText(width * invConfig.value.scaleY).toString(),
         ...tf.decompose(),
       });
     }
@@ -240,7 +241,7 @@ const axissInfo = computed(() => {
       const width = cur - prev;
       infos.top.texts.push({
         width: width,
-        text: getWidthText(width * invConfig.value.scaleX),
+        text: getWidthText(width * invConfig.value.scaleX).toString(),
         ...lt,
       });
     }
@@ -263,7 +264,7 @@ const axissInfo = computed(() => {
       const width = cur - prev;
       infos.bottom.texts.push({
         width: width,
-        text: getWidthText(width * invConfig.value.scaleX),
+        text: getWidthText(width * invConfig.value.scaleX).toString(),
         ...lt,
       });
     }

+ 39 - 19
src/core/hook/use-draw.ts

@@ -55,25 +55,26 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
   const conversion = useConversionPosition(true);
   const currentZIndex = useCurrentZIndex();
   const store = useStore();
-  let addCount = 0
+  let addCount = 0;
 
   let isEnter = false;
-  let modePop: (() => void) | undefined = void 0
+  let modePop: (() => void) | undefined = void 0;
   const enter = () => {
     if (!isEnter) {
       isEnter = true;
-      addCount = 0
+      addCount = 0;
       modePop = mode.push(Mode.draw);
     }
   };
   const leave = () => {
     if (isEnter) {
       isEnter = false;
-      modePop && modePop()
-      addCount = 0
+      modePop && modePop();
+      addCount = 0;
     }
   };
-  store.bus.on('addItemBefore', () => addCount++)
+  store.bus.on("addItemBefore", () => addCount++);
+  store.bus.on("setItemBefore", () => addCount++);
 
   return {
     delShape(id: string) {
@@ -131,12 +132,12 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       enter();
     },
     quitDrawShape: () => {
-      const currentAddCount = addCount
+      const currentAddCount = addCount;
       leave();
       interactiveProps.value = void 0;
-      return currentAddCount
+      return currentAddCount;
     },
-    drawing: computed(() => mode.include(Mode.draw))
+    drawing: computed(() => mode.include(Mode.draw)),
   };
 });
 
@@ -167,7 +168,7 @@ export const useDrawRunning = (shapeType?: ShapeType) => {
   return isRunning;
 };
 
-const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
+export const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
   const operMode = useOperMode();
   const conversionPosition = useConversionPosition(enableTransform);
   const snap = enableSnap && useSnap();
@@ -283,8 +284,17 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
     if (!item) return;
     item = reactive(item);
 
+    const storeAddItem = (cItem: any) => {
+      const items = store.getTypeItems(type);
+      if (items.some((item) => item.id === cItem.id)) {
+        store.setItem(type, { id: cItem.id, value: cItem });
+      } else {
+        store.addItem(type, cItem);
+      }
+    };
+
     if (ia.singleDone.value) {
-      store.addItem(type, item);
+      storeAddItem(item);
       return;
     }
 
@@ -317,7 +327,7 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
       // 监听是否消费完毕
       watch(ia.singleDone, () => {
         processorIds.push(item.id);
-        store.addItem(type, item);
+        storeAddItem(item);
         const ndx = items.indexOf(item);
         items.splice(ndx, 1);
         clear();
@@ -447,7 +457,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   });
 
   // 可能历史空间会撤销 重做更改到正在绘制的组件
-  const currentCursor = ref('/icons/m_add.png');
+  const currentCursor = ref("/icons/m_add.png");
   const cursor = useCursor();
   let cursorPop: ReturnType<typeof cursor.push> | null = null;
   let stopWatch: (() => void) | null = null;
@@ -503,13 +513,12 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
 
   const getAddMessage = (cur: Pos) => {
     let consumed = messages.value;
-    currentCursor.value = '/icons/m_add.png';
+    currentCursor.value = "/icons/m_add.png";
     let pen: null | ReturnType<typeof penUpdatePoints> = null;
     if (!operMode.value.freeDraw) {
       // pen = penUpdatePoints(messages.value, cur, type !== "polygon");
       // consumed = pen.points;
       // cur = pen.cur;
-      
     }
 
     return {
@@ -522,7 +531,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   const setMessage = (cur: Pos) => {
     const { pen, ...msg } = getAddMessage(cur);
     if ((currentIsDel = pen?.oper === "del")) {
-      currentCursor.value = '/icons/m_reduce.png';
+      currentCursor.value = "/icons/m_reduce.png";
       beforeHandler.clear();
     }
     return msg;
@@ -561,8 +570,17 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
       items[0] = item = reactive(item);
     }
 
+    const storeAddItem = (cItem: any) => {
+      const items = store.getTypeItems(type);
+      if (items.some((item) => item.id === cItem.id)) {
+        store.setItem(type, { id: cItem.id, value: cItem });
+      } else {
+        store.addItem(type, cItem);
+      }
+    };
+
     if (ia.singleDone.value) {
-      store.addItem(type, item);
+      storeAddItem(item);
       return;
     }
 
@@ -599,7 +617,9 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
         if (isChange) {
           if (firstEntry) {
             processorIds.push(item.id);
-            history.preventTrack(() => store.addItem(type, cItem));
+            history.preventTrack(() => {
+              storeAddItem(cItem);
+            });
           } else {
             store.setItem(type, { id: item.id, value: cItem });
           }
@@ -624,6 +644,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   return items;
 };
 
+
 export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
   const obj = components[type];
   if (obj.addMode === "dots") {
@@ -634,4 +655,3 @@ export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
     return useInteractiveDrawDots(type);
   }
 };
-

+ 4 - 3
src/core/hook/use-expose.ts

@@ -84,11 +84,12 @@ export const useShortcutKey = () => {
 
       const addCount = quitDrawShape();
       // 钢笔工具需要右键两次才退出,右键一次相当于完成
-      const isDots = components[iProps.type].addMode === "dots";
+      const isDots = ['dots', 'single-dots'].includes(components[iProps.type].addMode);
       if (isDots && addCount > 0) {
-        nextTick(() => {
+        console.log('quit')
+        setTimeout(() => {
           enterDrawShape(iProps.type, iProps.preset, iProps.operate?.single);
-        });
+        }, 10);
       }
     },
     document.documentElement

+ 5 - 5
src/core/hook/use-mouse-status.ts

@@ -4,7 +4,6 @@ import { Shape } from "konva/lib/Shape";
 import {
   globalWatch,
   installGlobalVar,
-  usePointerIntersection,
   usePointerIntersections,
   usePointerPos,
   useStage,
@@ -84,10 +83,11 @@ export const useShapeClick = (
     let downTime: number;
     let move = false;
     const downHandler = (ev: KonvaEventObject<any>) => {
-      // ev.evt.button === 0
-      ev.cancelBubble = true
-      downTime = Date.now();
-      move = false;
+      if (ev.evt.button === 0) {
+        ev.cancelBubble = true
+        downTime = Date.now();
+        move = false;
+      }
     };
     const moveHandler = (ev: KonvaEventObject<any>) => {
       ev.cancelBubble = true

+ 5 - 1
src/core/hook/use-proportion.ts

@@ -7,11 +7,15 @@ export const useProportion = installGlobalVar(() => {
   const proportion = computed(() => store.config.proportion);
 
   const transform = (width: number) => {
-    return Math.floor(width * proportion.value.scale).toString() + (proportion.value.unit || "");
+    return width * proportion.value.scale;
+  }
+  const invTransform = (u: number) => {
+    return u / proportion.value.scale;
   }
 
   return {
     proportion,
     transform,
+    invTransform
   }
 });

+ 2 - 2
src/core/hook/use-status.ts

@@ -87,8 +87,8 @@ export const useOperMode = installGlobalVar(() => {
 
   return computed(() => ({
     // 多选模式
-    // mulSelection: keys.has('Shift') && !keys.has(' ') && !keys.has('Alt'),
-    mulSelection: false,
+    mulSelection: keys.has('Shift') && !keys.has(' ') && !keys.has('Alt'),
+    // mulSelection: false,
     // 自由移动视图
     freeView: keys.has(' '),
     // 自由绘图,不吸附

+ 2 - 1
src/core/hook/use-transformer.ts

@@ -251,7 +251,8 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
       !result.isPause,
     (canEdit, _, onCleanup) => {
       canEdit && onCleanup(init(shape.value!.getNode()));
-    }
+    },
+    { immediate: true }
   );
   return result;
 };

+ 4 - 3
src/core/html-mount/propertys/components/num.vue

@@ -3,8 +3,8 @@
     <el-slider
       class="property-num-slider"
       :class="{ proportion: props.proportion }"
-      :modelValue="value"
-      @update:model-value="(val: any) => changeHandler(val)"
+      :modelValue="props.proportion ? transform(value) : value"
+      @update:model-value="(val: any) => props.proportion ? changeHandler(invTransform(val)) : changeHandler(val)"
       @change="$emit('change')"
       size="small"
       height="200px"
@@ -27,6 +27,7 @@ import { useProportion } from "@/core/hook/use-proportion";
 import { ElSlider } from "element-plus";
 
 const props = defineProps<{
+  data: Record<string, any>;
   value: number;
   min?: number;
   max?: number;
@@ -34,7 +35,7 @@ const props = defineProps<{
   proportion?: boolean;
 }>();
 
-const { proportion } = useProportion();
+const { proportion, transform, invTransform } = useProportion();
 
 const emit = defineEmits<{
   (e: "update:value", val: number): void;

+ 1 - 1
src/core/html-mount/propertys/describes.json

@@ -47,7 +47,7 @@
     "default": 1,
     "props": {
       "min": 0.1,
-      "max": 10
+      "max": 500
     },
     "layout-type": "column"
   },

+ 1 - 0
src/core/html-mount/propertys/mount.vue

@@ -20,6 +20,7 @@
           >
             <span class="label">{{ val.label }}</span>
             <component
+              :data="data"
               v-bind="{ ...(describes[key].props || {}), ...getPredefine(key) }"
               :value="
                 'value' in describes[key]

+ 3 - 2
src/core/renderer/draw-group.vue

@@ -12,8 +12,9 @@ import { ShapeType, components } from "../components";
 import { useInteractiveAdd } from "../hook/use-draw.ts";
 
 const props = defineProps<{ type: ShapeType }>();
-const tempItems = useInteractiveAdd(props.type);
 const type = props.type;
+const tempItems = components[type].useDraw
+  ? components[type].useDraw()
+  : useInteractiveAdd(props.type);
 const ShapeComponent = components[type].TempComponent || components[type].Component;
-
 </script>

+ 1 - 1
src/core/store/store.ts

@@ -21,7 +21,7 @@ export type StoreData = {
 };
 const defConfig: StoreData["config"] = {
   compass: { rotation: 0, url: 'icons/edit_compass.svg' },
-  proportion: {scale: 10, unit: ''}
+  proportion: {scale: 10, unit: 'mm'}
 };
 
 export const getEmptyStoreData = (): StoreData => {

+ 3 - 10
src/example/components/slide/actions.ts

@@ -32,7 +32,8 @@ export const draw: MenuItem = {
   name: "绘制",
   value: uuid(),
   children: [
-    { icon: "line", ...genDrawItem("line") },
+    { icon: "line", ...genDrawItem('sequentLine') },
+    { icon: "line", ...genDrawItem('line') },
     { icon: "arrows", ...genDrawItem("arrow") },
     { icon: "rectangle", ...genDrawItem("rectangle") },
     { icon: "circle", ...genDrawItem("circle") },
@@ -251,14 +252,6 @@ export const test: MenuItem = {
       handler: (draw: Draw) => {
         console.log(copy(draw.store.$state))
       },
-    },
-    {
-      value: uuid(),
-      icon: "",
-      name: "获取dxf",
-      handler: (draw: Draw) => {
-        draw.getDXFString()
-      },
-    },
+    }
   ],
 };

+ 144 - 0
src/example/fuse/enter.ts

@@ -0,0 +1,144 @@
+import type { TabCover } from "./store";
+import type { Scene } from "../platform/platform-resource";
+
+const SCENE_TYPE = {
+  fuse: "fuse",
+  mesh: "mesh",
+  cloud: "cloud",
+} as const;
+
+const getSceneList = async (): Promise<Scene[]> => [
+  {
+    m: 'SG-t-KclpWad2dW5',
+    title: "有AI",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+
+  },
+  {
+    m: "SG-t-jvab1SlVA1r",
+    title: "多楼层",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+  {
+    m: "SG-t-N6no657Kuze",
+    title: "单楼层",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+  {
+    m: "SS-t-hiNBZjf5EK8",
+    title: "庙堂火灾",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+  {
+    m: "SS-t-2tWYj9q5whZ",
+    title: "车行",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+  {
+    m: "SS-t-qnGLxHvngli",
+    title: "商品房",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+  {
+    m: "SG-t-lDhbbylq4sf",
+    title: "多楼层17.30",
+    id: onlyId(),
+    type: SCENE_TYPE.mesh,
+  },
+] as Scene[]
+
+const viewURLS = {
+  [SCENE_TYPE.mesh]: "https://test.4dkankan.com/spg.html?m={m}&lang=zh",
+  [SCENE_TYPE.cloud]:
+    "https://uat-laser.4dkankan.com/uat/index.html?m={m}&lang=zh",
+  [SCENE_TYPE.fuse]:
+    "https://test-mix3d.4dkankan.com/code/index.html?caseId={m}&app=1&share=1#/show/summary",
+};
+
+const resourceURLS = {
+  oss: "/meshOSS",
+  [SCENE_TYPE.mesh]: "/meshAPI",
+  [SCENE_TYPE.cloud]: "/cloudAPI",
+  [SCENE_TYPE.fuse]: "/fuseAPI",
+};
+
+import type { StoreData } from "@/core/store/store";
+import { onlyId } from "../../utils/shared";
+
+const getOverviewData = async () => {
+  const storeStr = localStorage.getItem("draw-data");
+  const store = (storeStr ? JSON.parse(storeStr) : {}) as StoreData;
+
+  const vportStr = localStorage.getItem("view-port");
+  const vport = (vportStr ? JSON.parse(vportStr) : null) as number[] | null;
+
+  return {
+    store,
+    viewport: vport,
+  };
+};
+
+const saveOverviewData = async (data: {
+  store: StoreData;
+  viewport: number[] | null;
+}) => {
+  localStorage.setItem("draw-data", JSON.stringify(data.store));
+  localStorage.setItem("view-port", JSON.stringify(data.viewport));
+};
+
+const getTabulationData = async () => {
+  const storeStr = localStorage.getItem("tab-draw-data");
+  const store = (storeStr ? JSON.parse(storeStr) : {}) as StoreData;
+
+  const vportStr = localStorage.getItem("tab-view-port");
+  const vport = (vportStr ? JSON.parse(vportStr) : null) as number[] | null;
+
+  const paperKeyStr = localStorage.getItem("tab-paper-key");
+  const paperKey = paperKeyStr ? JSON.parse(paperKeyStr) : "a4";
+
+  return {
+    store,
+    cover: tabCover,
+    paperKey,
+    viewport: vport,
+  };
+};
+
+const saveTabulationData = async (data: {
+  store: StoreData;
+  viewport: number[] | null;
+  paperKey?: string;
+}) => {
+  localStorage.setItem("tab-draw-data", JSON.stringify(data.store));
+  localStorage.setItem("tab-view-port", JSON.stringify(data.viewport));
+  localStorage.setItem("tab-paper-key", JSON.stringify(data.paperKey));
+};
+
+let tabCover: TabCover | null = null;
+const saveTabulationCover = async (data: TabCover) => {
+  tabCover = data;
+};
+
+const uploadResourse = async (file: File) => {
+  return URL.createObjectURL(file);
+};
+
+window.platform = {
+  resourceURLS,
+  viewURLS,
+  getOverviewData,
+  getSceneList,
+  saveOverviewData,
+  getTabulationData,
+  saveTabulationData,
+  saveTabulationCover,
+  uploadResourse,
+};
+/* @vite-ignore */
+import(import.meta.env.VITE_ENTRY_EXAMPLE);

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

@@ -40,6 +40,7 @@ const init = async (draw: Draw) => {
   draw.config.showLabelLine = true;
   draw.config.showComponentSize = true;
   draw.config.back = { color: "#f0f2f5", opacity: 1 };
+  draw.store.setConfig({ proportion: { scale: 10, unit: "mm" } });
   draw.store.setStore(overviewData.value.store);
   overviewData.value.viewport && draw.viewer.setViewMat(overviewData.value.viewport);
 };

+ 4 - 4
src/example/platform/platform-draw.ts

@@ -133,7 +133,7 @@ const drawLayerResource = (
       points: item,
     });
   });
-  draw.store.addItems("line", geos);
+  draw.store.addItems('sequentLine', geos);
 
   if (layerResource.thumb) {
     const box = layerResource.box;
@@ -169,8 +169,8 @@ const drawLayerResource = (
         url: item.url,
         lock: import.meta.env.DEV ? false : true,
         mat: [1, 0, 0, 1, item.position.x, item.position.y],
-        width: item.size ? item.size.width : 30,
-        height: item.size ? item.size.height : 30,
+        width: item.size ? item.size.width : 100,
+        height: item.size ? item.size.height : 100,
         // width: 30,
         // height: 30,
         cornerRadius: 0,
@@ -202,7 +202,7 @@ export const drawPlatformResource = (data: AIExposeData, draw: Draw) => {
 
   draw.history.onceTrack(() => {
     // draw.store.setConfig({ proportion: { scale: 10, unit: 'mm' } });
-    draw.store.setConfig({ proportion: { scale: 10, unit: "" } });
+    draw.store.setConfig({ proportion: { scale: 10, unit: "mm" } });
     for (const layer of layers) {
       // if (!draw.store.layers.includes(layer.name)) {
       //   draw.store.addLayer(layer.name);

+ 1 - 1
src/example/platform/platform-resource.ts

@@ -237,7 +237,7 @@ export const taggingGets = {
                 }
               }
             }
-
+            if (!name) return;
             tags.push({
               position: pos,
               url: `/icons/${icon ? icon : 'circle'}.svg`,