import { useStore } from "../store"; import { components, DrawItem, ShapeType, ComponentSnapInfo, } from "../components"; import { computed, reactive, Ref, ref, toRaw, watch, watchEffect } from "vue"; import { createLine, eqNGDire, eqPoint, lineIntersection, lineLen, linePointLen, linePointProjection, numEq, Pos, vector, vector2IncludedAngle, verticalVector, zeroEq, } from "@/utils/math"; import { globalWatch, installGlobalVar } from "./use-global-vars"; import { BaseItem } from "../components/util"; import { ScaleVectorType, useGetTransformerOperDirection, useGetTransformerOperType, useTransformer, } from "./use-transformer"; import { Transform } from "konva/lib/Util"; import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer"; import { MathUtils } from "three"; import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared"; import { useTestPoints } from "./use-debugger"; export type SnapInfo = ComponentSnapInfo & Pick; const useStoreSnapInfos = () => { const store = useStore(); const types = Object.keys(components) as ShapeType[]; const infos = reactive(new Set()); const cleanups = [] as Array<() => void>; for (const type of types) { const comp = components[type]; if (!("getSnapInfos" in comp)) continue; cleanups.push( globalWatch( () => store.getTypeItems(type), (items, _, onCleanup) => { if (!items) return; for (const item of items) { const snaps = computed(() => { if (item.ref) { return comp.getSnapInfos!(item as any) as SnapInfo[]; } else { return []; } }); const snapInfoWatchStop = watch( snaps, (snaps, _, onCleanup) => { snaps.forEach((snap) => { snap.id = item.id; infos.add(snap); }); onCleanup(() => { snaps.forEach((snap) => infos.delete(snap)); }); }, { immediate: true } ); const existsWatchStop = watchEffect(() => { if (!items.includes(item as any)) { snapInfoWatchStop(); existsWatchStop(); } }); onCleanup(() => { snapInfoWatchStop(); existsWatchStop(); }); } }, { immediate: true } ) ); } return { infos: computed(() => Array.from(infos.values())), cleanup: mergeFuns(cleanups), }; }; export const useCustomSnapInfos = installGlobalVar(() => { const infos = ref([]); return { infos, add: (snap: ComponentSnapInfo) => { infos.value.push(snap); }, remove: (snap: ComponentSnapInfo) => { const index = infos.value.findIndex((p) => toRaw(p) === toRaw(snap)); if (index !== -1) { infos.value.splice(index, 1); } }, }; }, Symbol('customSnapInfos')); export const useGlobalSnapInfos = installGlobalVar(() => { const storeInfos = useStoreSnapInfos(); const customInfos = useCustomSnapInfos(); return { var: computed( () => [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[] ), onDestroy: storeInfos.cleanup, }; }, Symbol("snapInfos")); export const useSnapConfig = () => { const unitTransform = useCacheUnitTransform(); return { get snapOffset() { return unitTransform.getPixel(10); }, }; }; export type SnapResultInfo = { attractSnaps: AttractSnapInfo[]; selfSnaps: ComponentSnapInfo[]; clear: () => void; }; export const useSnapResultInfo = installGlobalVar(() => { const snapInfo = reactive({ attractSnaps: [], selfSnaps: [], clear() { snapInfo.attractSnaps.length = 0; snapInfo.selfSnaps.length = 0; }, }) as SnapResultInfo; return snapInfo; }, Symbol("snapResultInfo")); export type AttractSnapInfo = { ref: ComponentSnapInfo; current: ComponentSnapInfo; offset: number; angle: number; join: Pos; refDirection: Pos; refLinkNdx: number; currentLinkNdx: number; }; // TODO 返回结果按照self.point参照线多少排序, 子数组按照offset排序 export const filterAttractSnapInfos = ( refInfos: ComponentSnapInfo[], selfInfos: ComponentSnapInfo[], filters: ( self: ComponentSnapInfo, ref: ComponentSnapInfo, result: AttractSnapInfo[] ) => | { items?: AttractSnapInfo[]; stop?: boolean; stopSelf?: boolean } | AttractSnapInfo[], sortKey?: "offset" | "angle" ) => { const attractSnapInfosGroups: AttractSnapInfo[][] = []; for (const self of selfInfos) { if (self.point.view) continue; const attractSnapInfos: AttractSnapInfo[] = []; for (const ref of refInfos) { let infos = filters(self, ref, attractSnapInfos); const stop = Array.isArray(infos) ? false : !!infos.stop; const stopRef = Array.isArray(infos) ? false : !!infos.stopSelf; infos = Array.isArray(infos) ? infos : infos.items!; if (infos && sortKey) { for (const info of infos) { arrayInsert( attractSnapInfos, info, (a, b) => b[sortKey] < a[sortKey] ); } } if (stop) { return attractSnapInfosGroups; } if (stopRef) { break; } } arrayInsert( attractSnapInfosGroups, attractSnapInfos, (a, b) => a.length < b.length ); } return attractSnapInfosGroups; }; type FilterAttrib = { maxOffset?: number; maxAngle?: number; type?: "inter" | "projection" | "all" | "none"; }; const getAttractSnapInfos = ( ref: ComponentSnapInfo, self: ComponentSnapInfo, filter: FilterAttrib ) => { filter.type = filter.type || "all"; const limitAngle = "maxAngle" in filter; const limitOffset = "maxOffset" in filter; const isAll = filter.type === "all"; const attractSnapInfos: AttractSnapInfo[] = []; for (let i = 0; i < self.linkAngle.length; i++) { for (let j = 0; j < ref.linkAngle.length; j++) { const angle = Math.abs(ref.linkAngle[j] - self.linkAngle[i]); if (limitAngle && angle > filter.maxAngle!) { continue; } let join: Pos | null = null; let offset: number | null = null; const checkOffset = (getJoin: () => Pos | null) => { join = getJoin(); offset = join && lineLen(self.point, join); const adopt = join && (!limitOffset || offset! < filter.maxOffset!); if (!adopt) { join = null; offset = null; } return adopt; }; if (filter.type === "none") { const adopt = checkOffset(() => ref.point); if (!adopt) continue; } else { const refLine = [ ref.point, vector(ref.point).add(ref.linkDirections[j]), ]; if (filter.type === "projection" || isAll) { const adopt = checkOffset(() => linePointProjection(refLine, self.point) ); if (!adopt && !isAll) continue; } const curLine = [ self.point, vector(self.point).add(self.linkDirections[i]), ]; if (!join && !checkOffset(() => lineIntersection(refLine, curLine))) { continue; } } attractSnapInfos.push({ ref, current: self, refLinkNdx: j, currentLinkNdx: i, angle, join: join!, offset: offset!, refDirection: ref.linkDirections[j], }); } } return attractSnapInfos; }; const moveSnap = ( refInfos: ComponentSnapInfo[], selfInfos: ComponentSnapInfo[], filter: FilterAttrib ) => { filter.maxOffset = filter.maxOffset || 15; const exclude = new Map(); const addExclude = (nor: AttractSnapInfo, act: AttractSnapInfo) => { exclude.set(nor, act); exclude.set(act, nor); }; const getAttractSnapJoin = (nor: AttractSnapInfo, act: AttractSnapInfo) => { if (nor === act || exclude.get(nor) === act) { return void 0; } if (eqPoint(nor.current.point, act.join)) { return addExclude(nor, act); } // console.log(nor.current.point, act.join) const norJoin = nor.join; const norDire = vector(nor.refDirection); const norLine = createLine(norJoin, norDire); // TODO 确保移动前后normal的 direction 保持一致 const useDire = act.refDirection; if (eqNGDire(norDire, useDire)) { return addExclude(nor, act); } const useJoin = act.join; const useLine = createLine(useJoin, useDire); const nuJoin = lineIntersection(norLine, useLine); if (!nuJoin || lineLen(nuJoin, norJoin) > filter.maxOffset!) { return addExclude(nor, act); } else { return nuJoin; } }; const useAttractSnaps: AttractSnapInfo[] = []; let end: Pos | null = null; let start: Pos | null = null; // TODO 最多参考2个信息 const attractSnapGroups = filterAttractSnapInfos( refInfos, selfInfos, (self, ref, selfGroup) => { const attractSnapInfos = getAttractSnapInfos(ref, self, filter); const nor = selfGroup[0] || attractSnapInfos[0]; const checks = [...selfGroup, ...attractSnapInfos]; // TODO 尽快提前结束 for (const check of checks) { const join = getAttractSnapJoin(nor, check); if (join) { end = join; start = nor.current.point; useAttractSnaps.push(nor, check); return { stop: true }; } } return attractSnapInfos; }, "offset" ); if (!end) { if (!attractSnapGroups.length || !attractSnapGroups[0].length) return null; const nor = attractSnapGroups[0][0]; end = nor.join; start = nor.current.point; useAttractSnaps.push(nor); // TODO 如果没有同一个点的两线段,则使用2垂直的两点线段 const move = vector(end!).sub(start!); for (let i = 0; i < attractSnapGroups.length; i++) { let j = i === 0 ? 1 : 0; for (; j < attractSnapGroups[i].length; j++) { const attractSnap = attractSnapGroups[i][j]; const rDire = attractSnap.refDirection; const angle = vector2IncludedAngle(nor.refDirection, rDire); if (!numEq(rangMod(angle, Math.PI), Math.PI / 2)) { continue; } const cPoint = vector(attractSnap.current.point).add(move); const rPoint = attractSnap.ref.point; const inter = lineIntersection( createLine(cPoint, nor.refDirection), createLine(rPoint, rDire) ); if (inter) { useAttractSnaps.push(attractSnap); end = vector(end).add(inter.sub(cPoint)); break; } } if (j !== attractSnapGroups[i].length) break; } } const norMove = vector(end!).sub(start!); return { useAttractSnaps, transform: new Transform().translate(norMove.x, norMove.y), }; }; type SelfAttitude = { rotation: number; origin: Pos; operTarget: Pos; center: Pos; }; const scaleSnap = ( refInfos: ComponentSnapInfo[], selfInfos: ComponentSnapInfo[], filter: Omit, type: ScaleVectorType, attitude: SelfAttitude, testPoints: Ref ) => { const { origin, operTarget } = attitude; const attractSnaps: AttractSnapInfo[] = []; const operVector = vector(operTarget).sub(origin).normalize(); const vOperVector = verticalVector(operVector); const vLine = createLine(origin, vOperVector); const proportional = [ "top-right", "top-left", "bottom-right", "bottom-left", ].includes(type); const limitOffset = filter.maxOffset; const asFilter: FilterAttrib = { ...filter, type: "none" }; delete asFilter.maxOffset; const exclude = new Set(); const getAttractSnapJoin = (nor: AttractSnapInfo) => { if (exclude.has(nor)) return; if ( eqNGDire(nor.refDirection, operVector) || eqNGDire( nor.refDirection, nor.current.linkDirections[nor.currentLinkNdx] ) || Math.abs( vector(origin).sub(nor.current.point).normalize().dot(operVector) ) < 0.01 ) { exclude.add(nor); return; } const cur = nor.current.point; if ( eqNGDire(vOperVector, nor.refDirection) && zeroEq(linePointLen(vLine, nor.ref.point)) ) { exclude.add(nor); attractSnaps.push(nor); return; } const refLine = [ nor.ref.point, vector(nor.ref.point).add(nor.refDirection), ]; const norLine = proportional ? [origin, cur] : [cur, vector(cur).add(operVector)]; const join = lineIntersection(refLine, norLine); if (!join || (limitOffset && lineLen(join, cur) > limitOffset)) { exclude.add(nor); return; } // testPoints.value = [origin, nor.ref.point]; return join; }; let useAttractSnap: AttractSnapInfo; let useJoin: Pos; // TODO 最多参考1个信息 filterAttractSnapInfos(refInfos, selfInfos, (self, ref) => { if (eqPoint(self.point, origin)) return { stopSelf: true }; const attractSnapInfos = getAttractSnapInfos(ref, self, asFilter); for (const info of attractSnapInfos) { const join = getAttractSnapJoin(info); if (join) { info.join = join; useJoin = join; useAttractSnap = info; return { stop: true }; } } return attractSnapInfos; }); if (!useAttractSnap! || !useJoin!) return null; const rotation = Math.atan2(operVector.y, operVector.x); const invRotateTransform = new Transform() .rotate(-rotation) .translate(-origin.x, -origin.y); const t = invRotateTransform.point(useJoin!); const c = invRotateTransform.point(useAttractSnap.current.point); const currentFactor = vector({ x: numEq(t.x, c.x) ? 1 : t.x / c.x, y: numEq(t.y, c.y) ? 1 : t.y / c.y, }); if (proportional) { currentFactor.y = currentFactor.x; } attractSnaps.push(useAttractSnap); return { useAttractSnaps: attractSnaps, transform: new Transform() .multiply(invRotateTransform.copy().invert()) .scale(currentFactor.x, currentFactor.y) .multiply(invRotateTransform), }; }; export const useSnap = ( snapInfos: { value: ComponentSnapInfo[] } = useGlobalSnapInfos() ) => { const snapResultInfo = useSnapResultInfo(); const snapConfig = useSnapConfig(); const testPoints = useTestPoints(); const afterHandler = (result: ReturnType) => { if (result) { snapResultInfo.attractSnaps = result.useAttractSnaps; return result.transform; } return null; }; const move = (selfSnapInfos: ComponentSnapInfo[]) => { const result = moveSnap(snapInfos.value, selfSnapInfos, { maxOffset: snapConfig.snapOffset, }); snapResultInfo.selfSnaps.push(...selfSnapInfos); return afterHandler(result); }; const scale = ( selfSnapInfos: ComponentSnapInfo[], attitude: SelfAttitude & { type: ScaleVectorType } ) => { const result = scaleSnap( snapInfos.value, selfSnapInfos, { maxOffset: snapConfig.snapOffset }, attitude.type, attitude, testPoints ); snapResultInfo.selfSnaps = selfSnapInfos; return afterHandler(result); }; return { move, scale, clear: snapResultInfo.clear, }; }; export const useComponentSnap = (componentId: string) => { const store = useStore(); const type = componentId ? store.getType(componentId) : undefined; const comp = type && components[type]; const api = type && comp?.getSnapInfos; if (!api) return null; const snapInfos = useGlobalSnapInfos(); const refSnapInfos = computed(() => snapInfos.value.filter((p) => !("id" in p) || p.id !== componentId) ); const baseSnap = useSnap(refSnapInfos); const getOperType = useGetTransformerOperType(); const getTransformerOperDirection = useGetTransformerOperDirection(); const transformer = useTransformer(); const viewerInvertTransform = useViewerInvertTransform(); const snap = (item: BaseItem) => { const operateType = getOperType(); const selfSnapInfos = api(item as any); baseSnap.clear(); // move if (!operateType) { return baseSnap.move(selfSnapInfos); } else if (operateType !== "rotater") { const direction = getTransformerOperDirection()!; const node = transformer.nodes()[0]; const rect = node.getClientRect(); const attitude = { center: viewerInvertTransform.value.point({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, }), operTarget: direction[1], rotation: MathUtils.degToRad(node.rotation()), origin: direction[0], type: operateType, }; return baseSnap.scale(selfSnapInfos, attitude); } }; return [snap, baseSnap.clear] as const; };