|
@@ -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,
|
|
|
- };
|
|
|
-};
|