Przeglądaj źródła

feat: 短线连接点制作

bill 2 miesięcy temu
rodzic
commit
3d83cfd852

+ 444 - 0
src/core/components/line/attach-server.ts

@@ -0,0 +1,444 @@
+import { SnapResultInfo, useSnapConfig } from "@/core/hook/use-snap";
+import { defaultStyle, LineData } from ".";
+import {
+  eqPoint,
+  getVectorLine,
+  lineIntersection,
+  lineLen,
+  linePointLen,
+  linePointProjection,
+  lineVector,
+  Pos,
+  vector2IncludedAngle,
+  verticalVector,
+  zeroEq,
+} from "@/utils/math";
+import { copy, onlyId, rangMod } from "@/utils/shared";
+import { MathUtils, Vector2 } from "three";
+import { getBaseItem } from "../util";
+
+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,
+  queUpdate = true
+) => {
+  for (let i = 0; i < data.lines.length; i++) {
+    const line = data.lines[i];
+    if (line.a === delId) {
+      if (queUpdate) {
+        data.lines[i] = { ...line, a: repId };
+      } else {
+        data.lines[i].a = repId;
+      }
+    }
+    if (line.b === delId) {
+      if (queUpdate) {
+        data.lines[i] = { ...line, b: repId };
+      } else {
+        data.lines[i].b = repId;
+      }
+    }
+  }
+  return data;
+};
+
+export const getLinePoints = (data: LineData, line: LineData["lines"][0]) => [
+  data.points.find((p) => p.id === line.a)!,
+  data.points.find((p) => p.id === line.b)!,
+];
+
+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 getJoinLine = (
+  data: LineData,
+  line: LineData["lines"][0],
+  pId: string
+) => {
+  const pointIds = [line.a, line.b];
+  return data.lines
+    .filter(
+      (item) =>
+        (item.a === pId || item.b === pId) &&
+        !(pointIds.includes(item.a) && pointIds.includes(item.b))
+    )
+    .map((line) => {
+      const pointIds = pId === line.a ? [line.a, line.b] : [line.b, line.a];
+      return {
+        ...line,
+        points: pointIds.map((id) => data.points.find((p) => p.id === id)!),
+      };
+    });
+};
+
+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--;
+      }
+    }
+  }
+
+  // 删除线a b 点一样的线段
+  for (let i = 0; i < data.lines.length; i++) {
+    const line = data.lines[i];
+    if (line.a === line.b) {
+      data.lines.splice(i--, 1);
+    }
+  }
+
+  // 删除游离点
+  const pointIds = Object.values(ctx.del.lines).flatMap((item) => [
+    item.a,
+    item.b,
+  ]);
+  pointIds.push(...Object.keys(ctx.add.points));
+  const linePointIds = data.lines.flatMap((item) => [item.a, item.b]);
+  for (let id of pointIds) {
+    if (!linePointIds.includes(id)) {
+      const ndx = data.points.findIndex((p) => p.id === id);
+      ~ndx && data.points.splice(ndx, 1);
+    }
+  }
+  return deduplicateLines(data);
+};
+
+export const genMoveLineHandler = (
+  data: LineData,
+  lineId: string,
+  snapConfig: ReturnType<typeof useSnapConfig>,
+  snapResult: SnapResultInfo,
+  ctx = getInitCtx()
+) => {
+  const line = data.lines.find((line) => line.id === lineId)!;
+  const pointIds = [line.a, line.b];
+  const points = pointIds.map((id) => data.points.find((p) => p.id === id)!);
+  const lineDire = lineVector(points);
+  const initPoints = copy(points);
+  const angleRange = [MathUtils.degToRad(10), MathUtils.degToRad(170)];
+
+  const getRefInfo = (moveDire: Vector2, ndx: number) => {
+    const joinLines = getJoinLine(data,  line, pointIds[ndx]);
+    const joinLineDires: Vector2[] = [];
+    const linePoints = [points[ndx], points[Number(!ndx)]];
+    const lineDire = lineVector(linePoints);
+    const joinPoints: LineData["points"] = [];
+
+    let invAngle = Number.MAX_VALUE;
+    let invSelectLineId: string;
+    let invSelectLineDire: Vector2 | null = null;
+
+    let alongAngle = -Number.MAX_VALUE;
+    let alongSelectLineId: string;
+    let alongSelectLineDire: Vector2 | null = null;
+
+    for (const line of joinLines) {
+      joinPoints.push(...line.points.filter((p) => !points.includes(p)));
+      const joinDire = lineVector(line.points);
+      joinLineDires.push(joinDire);
+
+      const angle = vector2IncludedAngle(lineDire, joinDire);
+      if (angle > 0) {
+        if (angle < invAngle) {
+          invAngle = angle;
+          invSelectLineId = line.id;
+          invSelectLineDire = joinDire;
+        }
+      } else {
+        if (angle > alongAngle) {
+          alongAngle = angle;
+          alongSelectLineId = line.id;
+          alongSelectLineDire = joinDire;
+        }
+      }
+    }
+
+    if (!invSelectLineDire && !alongSelectLineDire) {
+      return;
+    }
+    let isAlong = !invSelectLineDire;
+    if (!isAlong && alongSelectLineDire) {
+      const invMoveAngle = Math.abs(
+        vector2IncludedAngle(moveDire, invSelectLineDire!)
+      );
+      const alongMoveAngle = Math.abs(
+        vector2IncludedAngle(moveDire, alongSelectLineDire!)
+      );
+      isAlong = alongMoveAngle! < invMoveAngle!;
+    }
+    let info = isAlong
+      ? {
+          lineDire,
+          selectLineDire: alongSelectLineDire!,
+          selectLineId: alongSelectLineId!,
+          angle: alongAngle!,
+        }
+      : {
+          lineDire,
+          selectLineDire: invSelectLineDire!,
+          selectLineId: invSelectLineId!,
+          angle: invAngle!,
+        };
+
+    info.angle = rangMod(info.angle, Math.PI);
+    const needVertical =
+      info.angle > angleRange[1] || info.angle < angleRange[0];
+    const needSplit =
+      needVertical ||
+      joinLineDires.some(
+        (dire) =>
+          dire !== info.selectLineDire &&
+          !zeroEq(
+            rangMod(vector2IncludedAngle(dire, info.selectLineDire), Math.PI)
+          )
+      );
+    return { ...info, needSplit, needVertical, joinPoints };
+  };
+
+  let refInfos: ReturnType<typeof getRefInfo>[];
+  let snapLines: (null | Pos[])[];
+  let inited = false;
+  let norNdx = -1;
+
+  const init = (moveDires: Vector2[]) => {
+    refInfos = [getRefInfo(moveDires[0], 0), getRefInfo(moveDires[0], 1)];
+    snapLines = [];
+    let minAngle = Math.PI / 2;
+    const vLineDire = verticalVector(lineDire);
+    for (let i = 0; i < refInfos.length; i++) {
+      const refInfo = refInfos[i];
+      if (!refInfo) {
+        continue;
+      }
+      if (refInfo.needSplit) {
+        // 拆分点
+        const point = points[i];
+        const newPoint = { ...point, id: onlyId() };
+        data.points.push(newPoint);
+        repPointRef(data, point.id, newPoint.id, false);
+        const newLine = {
+          ...getBaseItem(),
+          ...defaultStyle,
+          a: point.id,
+          b: newPoint.id,
+        };
+        data.lines.push(newLine);
+        ctx.add.lines[newLine.id] = newLine;
+        ctx.add.points[newPoint.id] = newPoint;
+
+        if (i) {
+          line.b = point.id;
+        } else {
+          line.a = point.id;
+        }
+      }
+      const dire = refInfo.needVertical
+        ? verticalVector(refInfo.selectLineDire)
+        : refInfo.selectLineDire;
+
+      const angle = rangMod(vector2IncludedAngle(dire, vLineDire), Math.PI / 2);
+      if (angle < minAngle) {
+        norNdx = i;
+        minAngle = angle;
+      }
+      snapLines[i] = getVectorLine(dire, copy(points[i]), 10);
+    }
+  };
+  const assignPos = (origin: LineData["points"][0], target: Pos) => {
+    origin.x = target.x;
+    origin.y = target.y;
+    ctx.update.points[origin.id] = origin;
+  };
+
+  const updateOtPoint = (ndx: number) => {
+    const uNdx = ndx === 1 ? 0 : 1;
+    const move = new Vector2(
+      points[ndx].x - initPoints[ndx].x,
+      points[ndx].y - initPoints[ndx].y
+    );
+    if (!snapLines[uNdx]) {
+      assignPos(points[uNdx], move.add(initPoints[uNdx]));
+    } else {
+      assignPos(
+        points[uNdx],
+        lineIntersection(getVectorLine(lineDire, points[ndx]), snapLines[uNdx])!
+      );
+    }
+  };
+
+  const move = (finalPoss: Pos[]) => {
+    if (!inited) {
+      const moveDires = finalPoss.map((pos, ndx) =>
+        lineVector([initPoints[ndx], pos])
+      );
+      inited = true;
+      init(moveDires);
+    }
+
+    if (!snapLines[0] && !snapLines[1]) {
+      assignPos(points[0], finalPoss[0]);
+      assignPos(points[1], finalPoss[1]);
+    } else if (!snapLines[0]) {
+      const pos = linePointProjection(snapLines[1]!, finalPoss[1]);
+      assignPos(points[1], pos);
+      updateOtPoint(1);
+    } else if (!snapLines[1]) {
+      const pos = linePointProjection(snapLines[0]!, finalPoss[0]);
+      assignPos(points[0], pos);
+      updateOtPoint(0);
+    } else {
+      const pos = linePointProjection(snapLines[norNdx]!, finalPoss[norNdx]);
+      assignPos(points[norNdx], pos);
+      updateOtPoint(norNdx);
+    }
+  };
+
+  const getSnapRefPoint = (
+    point: Pos,
+    refPoints: Pos[],
+    line: Pos[] | null
+  ) => {
+    for (const refPoint of refPoints) {
+      if (
+        lineLen(refPoint, point) < snapConfig.snapOffset &&
+        (!line || zeroEq(linePointLen(line, refPoint)))
+      ) {
+        return refPoint;
+      }
+    }
+  };
+
+  const snap = () => {
+    snapResult.clear();
+    let refPoint: Pos | undefined = undefined;
+    let ndx = -1;
+
+    const useRefPoint = () => {
+      const hv = [
+        { join: refPoint, refDirection: { x: 0, y: 1 } },
+        { join: refPoint, refDirection: { x: 1, y: 0 } },
+      ];
+      snapResult.attractSnaps.push(...(hv as any));
+      assignPos(points[ndx], refPoint!);
+      updateOtPoint(ndx);
+    };
+
+    if (refInfos[0]?.joinPoints) {
+      refPoint = getSnapRefPoint(
+        points[0],
+        refInfos[0]?.joinPoints,
+        snapLines[0]
+      );
+      if (refPoint) {
+        ndx = 0;
+        return useRefPoint();
+      }
+    }
+    if (refInfos[1]?.joinPoints) {
+      refPoint = getSnapRefPoint(
+        points[1],
+        refInfos[1]?.joinPoints,
+        snapLines[1]
+      );
+      if (refPoint) {
+        ndx = 1;
+        return useRefPoint();
+      }
+    }
+
+    const usedPoints = [
+      ...(refInfos[0]?.joinPoints || []),
+      ...(refInfos[1]?.joinPoints || []),
+      ...points,
+    ];
+    const refPoints = data.points.filter((p) => !usedPoints.includes(p));
+    for (let i = 0; i < points.length; i++) {
+      refPoint = getSnapRefPoint(points[i], refPoints, snapLines[i]);
+      if (refPoint) {
+        ndx = i;
+        return useRefPoint();
+      }
+    }
+  };
+
+  const end = () => {
+    snapResult.clear();
+  };
+
+  return {
+    move: (ps: Pos[]) => {
+      move(ps);
+      snap();
+    },
+    end,
+  };
+};

+ 88 - 0
src/core/components/line/attach-view.ts

@@ -0,0 +1,88 @@
+import {
+  lineIntersection,
+  lineVector,
+  lineVerticalVector,
+  Pos,
+  vector2IncludedAngle,
+  verticalVector,
+} from "@/utils/math";
+import { LineData } from ".";
+import { getJoinLine, getLinePoints } from "./attach-server";
+import { MathUtils, Vector2 } from "three";
+import { computed, reactive, ref, Ref } from "vue";
+import { flatPositions, rangMod } from "@/utils/shared";
+
+const eloCacle = new WeakMap<LineData, string[]>();
+export const extendLinesOverlap = (
+  data: LineData,
+  line1: LineData["lines"][0]
+) => {
+  let cacle = eloCacle.get(data);
+  if (!cacle) {
+    eloCacle.set(data, (cacle = []));
+  }
+  const has = (ids: string[]) => {
+    const key = [...ids].sort().join("---");
+    return cacle.includes(key);
+  };
+  const set = (ids: string[]) => {
+    cacle.push([...ids].sort().join("---"));
+  };
+
+  type ELine = LineData["lines"][0] & { points: LineData["points"] };
+  const minAngle = MathUtils.degToRad(20);
+  const getExtendPolygon = (line1: ELine, line2: ELine) => {
+    const v1 = lineVector(line1.points);
+    const v2 = lineVector(line2.points);
+    const vv1 = verticalVector(v1);
+    const vv2 = verticalVector(v2);
+    const angle = vector2IncludedAngle(v1, v2);
+    const checkAngle = Math.abs(rangMod(angle, Math.PI));
+    if (checkAngle < minAngle || checkAngle > Math.PI - minAngle) return [];
+    const mul = angle > 0 ? -1 : 1;
+    const offset1 = vv1.multiplyScalar((line1.strokeWidth / 2) * mul);
+    const offset2 = vv2.multiplyScalar((line2.strokeWidth / 2) * mul);
+    const le1 = line1.points.map((p) => offset1.clone().add(p));
+    const le2 = line2.points.map((p) => offset2.clone().add(p));
+    const join = lineIntersection(le1, le2)!;
+
+    return [
+      {
+        points: flatPositions([le1[1], line1.points[1], join]),
+        fill: line1.stroke,
+        strokeWidth: 0.05,
+        stroke: line1.stroke,
+        closed: true,
+        listening: false,
+      },
+      {
+        points: flatPositions([le2[0], line1.points[1], join]),
+        fill: line2.stroke,
+        strokeWidth: 0.05,
+        stroke: line2.stroke,
+        closed: true,
+        listening: false,
+      },
+    ];
+  };
+
+  const points = [line1.a, line1.b].map(
+    (id) => data.points.find((item) => item.id === id)!
+  );
+
+  const joins: Ref<ReturnType<typeof getExtendPolygon>[]> = ref([]);
+  const groupPoints = [[...points], [...points].reverse()];
+  for (let i = 0; i < groupPoints.length; i++) {
+    const points = groupPoints[i];
+    const joinLines = getJoinLine(data, line1, points[1].id);
+
+    for (let j = 0; j < joinLines.length; j++) {
+      const key = [points[0].id, points[1].id, joinLines[j].points[1].id];
+      if (!has(key)) {
+        set(key);
+        joins.value.push(getExtendPolygon({ ...line1, points }, joinLines[i]))
+      }
+    }
+  }
+  return joins;
+};

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

@@ -5,11 +5,11 @@ import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { inRevise, onlyId, rangMod } from "@/utils/shared.ts";
 import { MathUtils } from "three";
 import { DrawStore, useStore } from "@/core/store/index.ts";
-import { getInitCtx, NLineDataCtx, normalLineData } from "./use-draw.ts";
 import { SelectionManageBus, UseGetSelectionManage } from "@/core/hook/use-selection.ts";
 import { EntityShape } from "@/deconstruction.js";
 import mitt from "mitt";
 import { watch } from "vue";
+import { getInitCtx, NLineDataCtx, normalLineData } from "./attach-server.ts";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";

+ 6 - 16
src/core/components/line/single-line.vue

@@ -13,18 +13,8 @@
     @dragend="emit('dragLineEnd', props.line)"
     @add-point="addPoint"
   />
-  <template v-if="joinLines">
-    <v-line
-      v-for="line in joinLines"
-      ref="line"
-      :config="{
-        strokeWidth: 20,
-        opacity: isDrawIng ? 0.5 : 1,
-        stroke: 'red',
-        points: flatPositions(line),
-        listening: false,
-      }"
-    />
+  <template v-for="polygons in joinsPolygons">
+    <v-line v-for="config in polygons" :config="config" />
   </template>
 
   <SizeLine
@@ -44,9 +34,9 @@
     <EditPoint
       v-for="(point, ndx) in points"
       :key="point.id"
-      :opacity="0.1"
       :size="line.strokeWidth"
       :points="points"
+      :opacity="0.1"
       :drawIng="ndx === 0 && isDrawIng"
       :ndx="ndx"
       :closed="false"
@@ -84,7 +74,6 @@ import { getMouseStyle, getSnapInfos, LineData, shapeName } from "./index.ts";
 import { flatPositions, onlyId } from "@/utils/shared.ts";
 import EditLine from "../share/edit-line.vue";
 import { getVectorLine, lineCenter, lineLen, lineVector, 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";
@@ -101,7 +90,8 @@ import {
 } from "@/core/hook/use-mouse-status.ts";
 import { themeColor } from "@/constant";
 import { Vector2 } from "three";
-import { extendLinesOverlap } from "./use-draw.ts";
+import { extendLinesOverlap } from "./attach-view.ts";
+import EditPoint from "../share/edit-point.vue";
 
 const mode = useMode();
 
@@ -113,7 +103,7 @@ const props = defineProps<{
   dragPointIds?: string[];
 }>();
 
-const joinLines = extendLinesOverlap(props.data, props.line);
+const joinsPolygons = extendLinesOverlap(props.data, props.line);
 
 const emit = defineEmits<{
   (e: "updatePoint", value: LineData["points"][number]): void;

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

@@ -27,13 +27,13 @@
 import { onlyId } from "@/utils/shared.ts";
 import { delPoint, LineData } from "./index.ts";
 import singleLine from "./single-line.vue";
+import { useInitData } from "./use-draw.ts";
 import {
   genMoveLineHandler,
   getInitCtx,
   NLineDataCtx,
   normalLineData,
-  useInitData,
-} from "./use-draw.ts";
+} from "./attach-server.ts";
 import { computed, ref } from "vue";
 import { useZIndex } from "@/core/hook/use-layer.ts";
 import { DC } from "@/deconstruction.js";
@@ -41,9 +41,7 @@ import { Group } from "konva/lib/Group";
 import { useOperMode } from "@/core/hook/use-status.ts";
 import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 import { Pos } from "@/utils/math.ts";
-import { useCustomSnapInfos, useSnap, useSnapConfig, useSnapResultInfo } from "@/core/hook/use-snap.ts";
-import { ComponentSnapInfo } from "../index.ts";
-import { generateSnapInfos } from "../util.ts";
+import { useSnapConfig, useSnapResultInfo } from "@/core/hook/use-snap.ts";
 
 const props = defineProps<{
   data: LineData;
@@ -108,7 +106,7 @@ const updateHandler = () => {
 };
 
 const resultInfo = useSnapResultInfo();
-const snapConfig = useSnapConfig()
+const snapConfig = useSnapConfig();
 let handler: ReturnType<typeof genMoveLineHandler>;
 const dragstartLineHandler = (line: LineData["lines"][0]) => {
   updateBeforeHandler([line.a, line.b]);

+ 5 - 495
src/core/components/line/use-draw.ts

@@ -13,28 +13,11 @@ 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, onlyId, rangMod } from "@/utils/shared";
-import {
-  eqPoint,
-  getVectorLine,
-  lineIntersection,
-  lineLen,
-  linePointLen,
-  linePointProjection,
-  lineVector,
-  Pos,
-  vector2IncludedAngle,
-  verticalVector,
-  zeroEq,
-} from "@/utils/math";
-import { defaultStyle, getSnapInfos, type LineData } from "./";
-import {
-  SnapResultInfo,
-  useCustomSnapInfos,
-  useSnapConfig,
-} from "@/core/hook/use-snap";
-import { MathUtils, Vector2 } from "three";
-import { getBaseItem } from "../util";
+import { copy, mergeFuns } from "@/utils/shared";
+import { Pos } from "@/utils/math";
+import { getSnapInfos, type LineData } from "./";
+import { useCustomSnapInfos } from "@/core/hook/use-snap";
+import { getInitCtx, normalLineData } from "./attach-server";
 
 type PayData = Pos;
 
@@ -305,476 +288,3 @@ export const useDraw = () => {
 
   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,
-  queUpdate = true
-) => {
-  for (let i = 0; i < data.lines.length; i++) {
-    const line = data.lines[i];
-    if (line.a === delId) {
-      if (queUpdate) {
-        data.lines[i] = { ...line, a: repId };
-      } else {
-        data.lines[i].a = repId;
-      }
-    }
-    if (line.b === delId) {
-      if (queUpdate) {
-        data.lines[i] = { ...line, b: repId };
-      } else {
-        data.lines[i].b = repId;
-      }
-    }
-  }
-  return data;
-};
-
-const eloCacle = new WeakMap<LineData, string[]>();
-export const extendLinesOverlap = (
-  data: LineData,
-  line1: LineData["lines"][0],
-) => {
-  const line1PIds = [line1.a, line1.b];
-  const line2 = data.lines.find(
-    (item) =>
-      (item !== line1 && line1PIds.includes(item.a)) ||
-      line1PIds.includes(item.b)
-  );
-  if (!line2) return;
-  const overlap = Math.max(line1.strokeWidth, line2.strokeWidth)
-  let eloeds = eloCacle.get(data);
-  if (!eloCacle.has(data)) {
-    eloCacle.set(data, (eloeds = []));
-  }
-  const key = line1.id + "j" + line2.id;
-  if (eloeds!.includes(key)) {
-    return;
-  } else {
-    eloeds!.push(key);
-  }
-
-  // 获取线段点
-  const pts1 = [
-    data.points.find((item) => item.id === line1.a)!,
-    data.points.find((item) => item.id === line1.b)!,
-  ];
-  const pts2 = [
-    data.points.find((item) => item.id === line2.a)!,
-    data.points.find((item) => item.id === line2.b)!,
-  ];
-
-  // 计算重叠向量
-  const dx = pts2[0].x - pts1[1].x;
-  const dy = pts2[0].y - pts1[1].y;
-  const len = Math.sqrt(dx * dx + dy * dy);
-  if (len === 0) {
-    return;
-  }
-
-  // 延长线段1的终点
-  const line1End = {
-    x: pts1[1].x + (dx / len) * overlap,
-    y: pts1[1].y + (dy / len) * overlap,
-  };
-
-  // 延长线段2的起点
-  const line2Start = {
-    x: pts2[0].x - (dx / len) * overlap,
-    y: pts2[0].y - (dy / len) * overlap,
-  };
-  return [
-    [pts1[1], line1End],
-    // [line2Start, pts2[0]],
-  ];
-};
-
-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--;
-      }
-    }
-  }
-
-  // 删除线a b 点一样的线段
-  for (let i = 0; i < data.lines.length; i++) {
-    const line = data.lines[i];
-    if (line.a === line.b) {
-      data.lines.splice(i--, 1);
-    }
-  }
-
-  // 删除游离点
-  const pointIds = Object.values(ctx.del.lines).flatMap((item) => [
-    item.a,
-    item.b,
-  ]);
-  pointIds.push(...Object.keys(ctx.add.points));
-  const linePointIds = data.lines.flatMap((item) => [item.a, item.b]);
-  for (let id of pointIds) {
-    if (!linePointIds.includes(id)) {
-      const ndx = data.points.findIndex((p) => p.id === id);
-      ~ndx && data.points.splice(ndx, 1);
-    }
-  }
-  return deduplicateLines(data);
-};
-
-export const genMoveLineHandler = (
-  data: LineData,
-  lineId: string,
-  snapConfig: ReturnType<typeof useSnapConfig>,
-  snapResult: SnapResultInfo,
-  ctx = getInitCtx()
-) => {
-  const line = data.lines.find((line) => line.id === lineId)!;
-  const pointIds = [line.a, line.b];
-  const points = pointIds.map((id) => data.points.find((p) => p.id === id)!);
-  const lineDire = lineVector(points);
-  const initPoints = copy(points);
-  const angleRange = [MathUtils.degToRad(10), MathUtils.degToRad(170)];
-  const getJoinLine = (pId: string) =>
-    data.lines
-      .filter(
-        (item) =>
-          (item.a === pId || item.b === pId) &&
-          !(pointIds.includes(item.a) && pointIds.includes(item.b))
-      )
-      .map((line) => {
-        const pointIds = pId === line.a ? [line.a, line.b] : [line.b, line.a];
-        return {
-          id: line.id,
-          points: pointIds.map((id) => data.points.find((p) => p.id === id)!),
-        };
-      });
-
-  const getRefInfo = (moveDire: Vector2, ndx: number) => {
-    const joinLines = getJoinLine(pointIds[ndx]);
-    const joinLineDires: Vector2[] = [];
-    const line = [points[ndx], points[Number(!ndx)]];
-    const lineDire = lineVector(line);
-    const joinPoints: LineData["points"] = [];
-
-    let invAngle = Number.MAX_VALUE;
-    let invSelectLineId: string;
-    let invSelectLineDire: Vector2 | null = null;
-
-    let alongAngle = -Number.MAX_VALUE;
-    let alongSelectLineId: string;
-    let alongSelectLineDire: Vector2 | null = null;
-
-    for (const line of joinLines) {
-      joinPoints.push(...line.points.filter((p) => !points.includes(p)));
-      const joinDire = lineVector(line.points);
-      joinLineDires.push(joinDire);
-
-      const angle = vector2IncludedAngle(lineDire, joinDire);
-      if (angle > 0) {
-        if (angle < invAngle) {
-          invAngle = angle;
-          invSelectLineId = line.id;
-          invSelectLineDire = joinDire;
-        }
-      } else {
-        if (angle > alongAngle) {
-          alongAngle = angle;
-          alongSelectLineId = line.id;
-          alongSelectLineDire = joinDire;
-        }
-      }
-    }
-
-    if (!invSelectLineDire && !alongSelectLineDire) {
-      return;
-    }
-    let isAlong = !invSelectLineDire;
-    if (!isAlong && alongSelectLineDire) {
-      const invMoveAngle = Math.abs(
-        vector2IncludedAngle(moveDire, invSelectLineDire!)
-      );
-      const alongMoveAngle = Math.abs(
-        vector2IncludedAngle(moveDire, alongSelectLineDire!)
-      );
-      isAlong = alongMoveAngle! < invMoveAngle!;
-    }
-    let info = isAlong
-      ? {
-          lineDire,
-          selectLineDire: alongSelectLineDire!,
-          selectLineId: alongSelectLineId!,
-          angle: alongAngle!,
-        }
-      : {
-          lineDire,
-          selectLineDire: invSelectLineDire!,
-          selectLineId: invSelectLineId!,
-          angle: invAngle!,
-        };
-
-    info.angle = rangMod(info.angle, Math.PI);
-    const needVertical =
-      info.angle > angleRange[1] || info.angle < angleRange[0];
-    const needSplit =
-      needVertical ||
-      joinLineDires.some(
-        (dire) =>
-          dire !== info.selectLineDire &&
-          !zeroEq(
-            rangMod(vector2IncludedAngle(dire, info.selectLineDire), Math.PI)
-          )
-      );
-    return { ...info, needSplit, needVertical, joinPoints };
-  };
-
-  let refInfos: ReturnType<typeof getRefInfo>[];
-  let snapLines: (null | Pos[])[];
-  let inited = false;
-  let norNdx = -1;
-
-  const init = (moveDires: Vector2[]) => {
-    refInfos = [getRefInfo(moveDires[0], 0), getRefInfo(moveDires[0], 1)];
-    snapLines = [];
-    let minAngle = Math.PI / 2;
-    const vLineDire = verticalVector(lineDire);
-    for (let i = 0; i < refInfos.length; i++) {
-      const refInfo = refInfos[i];
-      if (!refInfo) {
-        continue;
-      }
-      if (refInfo.needSplit) {
-        // 拆分点
-        const point = points[i];
-        const newPoint = { ...point, id: onlyId() };
-        data.points.push(newPoint);
-        repPointRef(data, point.id, newPoint.id, false);
-        const newLine = {
-          ...getBaseItem(),
-          ...defaultStyle,
-          a: point.id,
-          b: newPoint.id,
-        };
-        data.lines.push(newLine);
-        ctx.add.lines[newLine.id] = newLine;
-        ctx.add.points[newPoint.id] = newPoint;
-
-        if (i) {
-          line.b = point.id;
-        } else {
-          line.a = point.id;
-        }
-      }
-      const dire = refInfo.needVertical
-        ? verticalVector(refInfo.selectLineDire)
-        : refInfo.selectLineDire;
-
-      const angle = rangMod(vector2IncludedAngle(dire, vLineDire), Math.PI / 2);
-      if (angle < minAngle) {
-        norNdx = i;
-        minAngle = angle;
-      }
-      snapLines[i] = getVectorLine(dire, copy(points[i]), 10);
-    }
-  };
-  const assignPos = (origin: LineData["points"][0], target: Pos) => {
-    origin.x = target.x;
-    origin.y = target.y;
-    ctx.update.points[origin.id] = origin;
-  };
-
-  const updateOtPoint = (ndx: number) => {
-    const uNdx = ndx === 1 ? 0 : 1;
-    const move = new Vector2(
-      points[ndx].x - initPoints[ndx].x,
-      points[ndx].y - initPoints[ndx].y
-    );
-    if (!snapLines[uNdx]) {
-      assignPos(points[uNdx], move.add(initPoints[uNdx]));
-    } else {
-      assignPos(
-        points[uNdx],
-        lineIntersection(getVectorLine(lineDire, points[ndx]), snapLines[uNdx])!
-      );
-    }
-  };
-
-  const move = (finalPoss: Pos[]) => {
-    if (!inited) {
-      const moveDires = finalPoss.map((pos, ndx) =>
-        lineVector([initPoints[ndx], pos])
-      );
-      inited = true;
-      init(moveDires);
-    }
-
-    if (!snapLines[0] && !snapLines[1]) {
-      assignPos(points[0], finalPoss[0]);
-      assignPos(points[1], finalPoss[1]);
-    } else if (!snapLines[0]) {
-      const pos = linePointProjection(snapLines[1]!, finalPoss[1]);
-      assignPos(points[1], pos);
-      updateOtPoint(1);
-    } else if (!snapLines[1]) {
-      const pos = linePointProjection(snapLines[0]!, finalPoss[0]);
-      assignPos(points[0], pos);
-      updateOtPoint(0);
-    } else {
-      const pos = linePointProjection(snapLines[norNdx]!, finalPoss[norNdx]);
-      assignPos(points[norNdx], pos);
-      updateOtPoint(norNdx);
-    }
-  };
-
-  const getSnapRefPoint = (
-    point: Pos,
-    refPoints: Pos[],
-    line: Pos[] | null
-  ) => {
-    for (const refPoint of refPoints) {
-      if (
-        lineLen(refPoint, point) < snapConfig.snapOffset &&
-        (!line || zeroEq(linePointLen(line, refPoint)))
-      ) {
-        return refPoint;
-      }
-    }
-  };
-
-  const snap = () => {
-    snapResult.clear();
-    let refPoint: Pos | undefined = undefined;
-    let ndx = -1;
-
-    const useRefPoint = () => {
-      const hv = [
-        { join: refPoint, refDirection: { x: 0, y: 1 } },
-        { join: refPoint, refDirection: { x: 1, y: 0 } },
-      ];
-      snapResult.attractSnaps.push(...(hv as any));
-      assignPos(points[ndx], refPoint!);
-      updateOtPoint(ndx);
-    };
-
-    if (refInfos[0]?.joinPoints) {
-      refPoint = getSnapRefPoint(
-        points[0],
-        refInfos[0]?.joinPoints,
-        snapLines[0]
-      );
-      if (refPoint) {
-        ndx = 0;
-        return useRefPoint();
-      }
-    }
-    if (refInfos[1]?.joinPoints) {
-      refPoint = getSnapRefPoint(
-        points[1],
-        refInfos[1]?.joinPoints,
-        snapLines[1]
-      );
-      if (refPoint) {
-        ndx = 1;
-        return useRefPoint();
-      }
-    }
-
-    const usedPoints = [
-      ...(refInfos[0]?.joinPoints || []),
-      ...(refInfos[1]?.joinPoints || []),
-      ...points,
-    ];
-    const refPoints = data.points.filter((p) => !usedPoints.includes(p));
-    for (let i = 0; i < points.length; i++) {
-      refPoint = getSnapRefPoint(points[i], refPoints, snapLines[i]);
-      if (refPoint) {
-        ndx = i;
-        return useRefPoint();
-      }
-    }
-  };
-
-  const end = () => {
-    snapResult.clear();
-  };
-
-  return {
-    move: (ps: Pos[]) => {
-      move(ps);
-      snap();
-    },
-    end,
-  };
-};

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

@@ -1,10 +1,13 @@
-import { copy, genBound, onlyId } from "@/utils/shared";
+import { genBound, onlyId } from "@/utils/shared";
 import { AIExposeData } from "../dialog/ai";
 import { Draw } from "../components/container/use-draw";
 import { SceneFloor } from "./platform-resource";
 import { getBaseItem } from "@/core/components/util";
 import { LineData, defaultStyle } from "@/core/components/line";
-import { getInitCtx, normalLineData } from "@/core/components/line/use-draw";
+import {
+  getInitCtx,
+  normalLineData,
+} from "@/core/components/line/attach-server";
 import {
   getIconStyle,
   defaultStyle as iconDefaultStyle,
@@ -18,7 +21,7 @@ import { watchEffect } from "vue";
 import { TextData } from "@/core/components/text";
 
 const scaleResource = (info: AIExposeData, scale: number) => {
-  console.log(info.taggings)
+  console.log(info.taggings);
   const floors = info.floors.map((item) => ({
     ...item,
     geos: item.geos.map((geo) =>
@@ -316,7 +319,7 @@ const drawLayerResource = async (
             item.url,
             item.size?.width,
             item.size?.height,
-            item.fixed,
+            item.fixed
           );
         } catch {}
       }