import { SnapResultInfo, useCustomSnapInfos, useSnap, useSnapConfig, } from "@/core/hook/use-snap"; import { defaultStyle, getSnapInfos, LineData, LineDataLine } from "."; import { eqPoint, getVectorLine, lineCenter, lineIntersection, lineLen, linePointLen, linePointProjection, lineVector, Pos, vector2IncludedAngle, verticalVector, zeroEq, } from "@/utils/math"; import { copy, frameEebounce, mergeFuns, onlyId, rangMod, } from "@/utils/shared"; import { MathUtils, Vector2 } from "three"; import { generateSnapInfos, getBaseItem } from "../util"; import { ComponentSnapInfo } from ".."; import { useStore } from "@/core/store"; import { computed, onUnmounted, reactive, ref, Ref, watch } from "vue"; import { useCursor, usePointerPos, useRunHook, useStage, } from "@/core/hook/use-global-vars"; import { PropertyDescribes } from "@/core/html-mount/propertys"; import { useMode } from "@/core/hook/use-status"; import { useListener } from "@/core/hook/use-event"; import { Mode } from "@/constant/mode"; import { useViewerInvertTransform } from "@/core/hook/use-viewer"; import { clickListener } from "@/utils/event"; import { genGetLineIconAttach, getLineIconMat, LineIconData, } from "../line-icon"; import { useDrawIngData } from "@/core/hook/use-draw"; import { useComponentDescribes } from "@/core/hook/use-component"; export type NLineDataCtx = { del: { points: Record; lines: Record; }; add: { points: Record; lines: Record; }; update: { points: Record; lines: Record; }; }; 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: LineDataLine) => [ 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(); 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: LineDataLine, 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 foreNormalLineData = (data:LineData) => { for (let i = 0; i < data.lines.length; i++) { const {a, b} = data.lines[i] if (!data.points.some(p => p.id === a) || !data.points.some(p => p.id === b)) { data.lines.splice(i--, 1) } } for (let i = 0; i < data.points.length; i++) { const id = data.points[i].id if (!data.lines.some(l => l.a === id || l.b === id)) { data.points.splice(i, 1) } } } 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); } } // foreNormalLineData(data) return deduplicateLines(data); }; export const genMoveLineHandler = ( data: LineData, lineId: string, snapConfig: ReturnType, 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[]; 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, }; }; export const useLineDataSnapInfos = () => { const infos = useCustomSnapInfos(); const store = useStore(); const lineData = computed(() => store.getTypeItems("line")[0]); let snapInfos: ComponentSnapInfo[]; const updateSnapInfos = (pointIds: string[]) => { clear(); snapInfos = getSnapInfos({ ...lineData.value, lines: lineData.value.lines.filter( (item) => !(pointIds.includes(item.a) || pointIds.includes(item.b)) ), points: lineData.value.points.filter( (item) => !pointIds.includes(item.id) ), }); snapInfos.forEach((item) => { infos.add(item); }); }; const clear = () => { snapInfos && snapInfos.forEach((item) => infos.remove(item)); }; return { update: updateSnapInfos, clear, }; }; export const updateLineLength = ( lineData: LineData, line: LineDataLine, length: number, flex?: "a" | "b" | "both", vector?: Pos ) => { const points = [ lineData.points.find((p) => p.id === line.a)!, lineData.points.find((p) => p.id === line.b)!, ]; vector = vector || lineVector(points); if (!flex) { const aCount = lineData.lines.filter( (line) => line.a === points[0].id || line.b === points[0].id ).length; const bCount = lineData.lines.filter( (line) => line.a === points[1].id || line.b === points[1].id ).length; if (aCount === bCount || (aCount > 1 && bCount > 1)) { flex = "both"; } else { flex = aCount > 1 ? "b" : "a"; } } let moveVector = new Vector2(vector.x, vector.y); let npoints: Pos[]; if (flex === "both") { const center = lineCenter(points); const l1 = getVectorLine( moveVector.clone().multiplyScalar(-1), center, length / 2 ); const l2 = getVectorLine(moveVector, center, length / 2); npoints = [l1[1], l2[1]]; } else { const fNdx = flex === "a" ? 1 : 0; const mNdx = flex === "a" ? 0 : 1; const line = getVectorLine( mNdx === 1 ? moveVector : moveVector.multiplyScalar(-1), points[fNdx], length ); const nPoints: Pos[] = []; nPoints[fNdx] = points[fNdx]; nPoints[mNdx] = line[1]; npoints = nPoints; } Object.assign(points[0], npoints[0]); Object.assign(points[1], npoints[1]); }; export const useLineDescribes = (line: Ref) => { const d: any = useComponentDescribes(line, ["stroke", "strokeWidth"], {}); const store = useStore(); const lineData = computed(() => store.getTypeItems("line")[0]); const points = computed(() => [ lineData.value.points.find((p) => p.id === line.value.a)!, lineData.value.points.find((p) => p.id === line.value.b)!, ]); let setLineVector: Vector2; watch(d, (d) => { d.strokeWidth.props = { ...d.strokeWidth.props, proportion: true, }; d.strokeWidth.label = "粗细"; d.stroke.label = "颜色"; d.length = { type: "inputNum", label: "线段长度", "layout-type": "row", props: { proportion: true, }, get value() { return lineLen(points.value[0], points.value[1]); }, set value(val) { console.log(val, d.length.isChange); if (!d.isChange) { setLineVector = lineVector(points.value); } updateLineLength( lineData.value, line.value, val, undefined, setLineVector ); }, }; }, {immediate: true}); return d as PropertyDescribes; }; export const useDrawLinePoint = ( data: Ref, line: Ref, callback: (data: { prev: LineDataLine; next: LineDataLine; point: LineData["points"][0]; oldIcons: LineIconData[]; newIcons: LineIconData[]; }) => void ) => { const mode = useMode(); let __leave: (() => void) | null; const leave = () => { if (__leave) { __leave(); __leave = null; } }; useListener("contextmenu", (ev) => ev.button === 2 && setTimeout(leave)); onUnmounted(leave); const pos = usePointerPos(); const viewInvMat = useViewerInvertTransform(); const drawProps = ref<{ data: LineData; prev: LineDataLine; next: LineDataLine; point: LineData["points"][0]; }>(); const runHook = useRunHook(); const snapInfos = useLineDataSnapInfos(); const snap = useSnap(); const stage = useStage(); const store = useStore(); const icons = computed(() => store.getTypeItems("lineIcon").filter((item) => item.lineId === line.value.id) ); const drawStore = useDrawIngData(); const cursor = useCursor(); const enterDraw = () => { const points = getLinePoints(data.value, line.value) console.log(points, data.value, line.value) const cdata: LineData = { ...data.value, points, lines: [] }; const point = reactive({ ...lineCenter(points), id: onlyId() }); const cIcons = icons.value.map((icon) => ({ ...icon, id: onlyId() })); const iconInfos = icons.value.map((icon) => { const mat = getLineIconMat(points, icon); return { position: { x: mat.m[4], y: mat.m[5] }, size: { width: icon.endLen - icon.startLen, height: icon.height, }, }; }); const prev = { ...line.value, id: onlyId(), b: point.id }; const next = { ...line.value, id: onlyId(), a: point.id }; cdata.lines.push(prev, next); cdata.points.push(point); drawProps.value = { data: cdata, prev, next, point }; let isStop = false; const afterUpdate = frameEebounce((position: Pos) => { if (isStop) return; snap.clear(); position = viewInvMat.value.point(position); const mat = snap.move(generateSnapInfos([position], true, true)); Object.assign(point, mat ? mat.point(position) : position); drawStore.lineIcon = []; cIcons.forEach((icon, ndx) => { const getAttach = genGetLineIconAttach(cdata, iconInfos[ndx].size, 200); const attach = getAttach(iconInfos[ndx].position); if (attach) { const line = cdata.lines.find((item) => item.id === attach.lineId)!; const snapLine = [ cdata.points.find((p) => p.id === line.a)!, cdata.points.find((p) => p.id === line.b)!, ]; const iconData = { ...icon, ...attach, __snapLine: snapLine }; drawStore.lineIcon!.push(iconData); } }); }); pos.replay(); snapInfos.update([]); return mergeFuns( cursor.push("./icons/m_add.png"), runHook(() => clickListener(stage.value!.getNode().container(), () => { callback({ prev, next, point, oldIcons: icons.value, newIcons: drawStore.lineIcon!, }); leave(); }) ), watch( pos, (pos) => { pos && afterUpdate(pos); }, { immediate: true } ), () => { drawProps.value = undefined; snapInfos.clear(); snap.clear(); isStop = true; } ); }; const enter = () => { __leave = mergeFuns( () => (__leave = null), mode.push(Mode.draw), watch( () => mode.include(Mode.draw), (hasDraw, _, onCleanup) => { hasDraw ? onCleanup(enterDraw()) : leave(); }, { immediate: true } ) ); }; return { leave, enter, drawProps, }; };