use-snap.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. import { useStore } from "../store";
  2. import {
  3. components,
  4. DrawItem,
  5. ShapeType,
  6. ComponentSnapInfo,
  7. } from "../components";
  8. import { computed, reactive, Ref, ref, toRaw, watch, watchEffect } from "vue";
  9. import {
  10. createLine,
  11. eqNGDire,
  12. eqPoint,
  13. lineIntersection,
  14. lineLen,
  15. linePointLen,
  16. linePointProjection,
  17. numEq,
  18. Pos,
  19. vector,
  20. vector2IncludedAngle,
  21. verticalVector,
  22. zeroEq,
  23. } from "@/utils/math";
  24. import { globalWatch, installGlobalVar } from "./use-global-vars";
  25. import { BaseItem } from "../components/util";
  26. import {
  27. ScaleVectorType,
  28. useGetTransformerOperDirection,
  29. useGetTransformerOperType,
  30. useTransformer,
  31. } from "./use-transformer";
  32. import { Transform } from "konva/lib/Util";
  33. import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
  34. import { MathUtils } from "three";
  35. import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";
  36. import { useTestPoints } from "./use-debugger";
  37. export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;
  38. const useStoreSnapInfos = () => {
  39. const store = useStore();
  40. const types = Object.keys(components) as ShapeType[];
  41. const infos = reactive(new Set<SnapInfo>());
  42. const cleanups = [] as Array<() => void>;
  43. for (const type of types) {
  44. const comp = components[type];
  45. if (!("getSnapInfos" in comp)) continue;
  46. cleanups.push(
  47. globalWatch(
  48. () => store.getTypeItems(type),
  49. (items, _, onCleanup) => {
  50. if (!items) return;
  51. for (const item of items) {
  52. const snaps = computed(() => {
  53. if (item.ref) {
  54. return comp.getSnapInfos!(item as any) as SnapInfo[];
  55. } else {
  56. return [];
  57. }
  58. });
  59. const snapInfoWatchStop = watch(
  60. snaps,
  61. (snaps, _, onCleanup) => {
  62. snaps.forEach((snap) => {
  63. snap.id = item.id;
  64. infos.add(snap);
  65. });
  66. onCleanup(() => {
  67. snaps.forEach((snap) => infos.delete(snap));
  68. });
  69. },
  70. { immediate: true }
  71. );
  72. const existsWatchStop = watchEffect(() => {
  73. if (!items.includes(item as any)) {
  74. snapInfoWatchStop();
  75. existsWatchStop();
  76. }
  77. });
  78. onCleanup(() => {
  79. snapInfoWatchStop();
  80. existsWatchStop();
  81. });
  82. }
  83. },
  84. { immediate: true }
  85. )
  86. );
  87. }
  88. return {
  89. infos: computed(() => Array.from(infos.values())),
  90. cleanup: mergeFuns(cleanups),
  91. };
  92. };
  93. export const useCustomSnapInfos = installGlobalVar(() => {
  94. const infos = ref<ComponentSnapInfo[]>([]);
  95. return {
  96. infos,
  97. add: (snap: ComponentSnapInfo) => {
  98. infos.value.push(snap);
  99. },
  100. remove: (snap: ComponentSnapInfo) => {
  101. const index = infos.value.findIndex((p) => toRaw(p) === toRaw(snap));
  102. if (index !== -1) {
  103. infos.value.splice(index, 1);
  104. }
  105. },
  106. };
  107. }, Symbol('customSnapInfos'));
  108. export const useGlobalSnapInfos = installGlobalVar(() => {
  109. const storeInfos = useStoreSnapInfos();
  110. const customInfos = useCustomSnapInfos();
  111. return {
  112. var: computed(
  113. () =>
  114. [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]
  115. ),
  116. onDestroy: storeInfos.cleanup,
  117. };
  118. }, Symbol("snapInfos"));
  119. export const useSnapConfig = () => {
  120. const unitTransform = useCacheUnitTransform();
  121. return {
  122. get snapOffset() {
  123. return unitTransform.getPixel(10);
  124. },
  125. };
  126. };
  127. export type SnapResultInfo = {
  128. attractSnaps: AttractSnapInfo[];
  129. selfSnaps: ComponentSnapInfo[];
  130. clear: () => void;
  131. };
  132. export const useSnapResultInfo = installGlobalVar(() => {
  133. const snapInfo = reactive({
  134. attractSnaps: [],
  135. selfSnaps: [],
  136. clear() {
  137. snapInfo.attractSnaps.length = 0;
  138. snapInfo.selfSnaps.length = 0;
  139. },
  140. }) as SnapResultInfo;
  141. return snapInfo;
  142. }, Symbol("snapResultInfo"));
  143. export type AttractSnapInfo = {
  144. ref: ComponentSnapInfo;
  145. current: ComponentSnapInfo;
  146. offset: number;
  147. angle: number;
  148. join: Pos;
  149. refDirection: Pos;
  150. refLinkNdx: number;
  151. currentLinkNdx: number;
  152. };
  153. // TODO 返回结果按照self.point参照线多少排序, 子数组按照offset排序
  154. export const filterAttractSnapInfos = (
  155. refInfos: ComponentSnapInfo[],
  156. selfInfos: ComponentSnapInfo[],
  157. filters: (
  158. self: ComponentSnapInfo,
  159. ref: ComponentSnapInfo,
  160. result: AttractSnapInfo[]
  161. ) =>
  162. | { items?: AttractSnapInfo[]; stop?: boolean; stopSelf?: boolean }
  163. | AttractSnapInfo[],
  164. sortKey?: "offset" | "angle"
  165. ) => {
  166. const attractSnapInfosGroups: AttractSnapInfo[][] = [];
  167. for (const self of selfInfos) {
  168. if (self.point.view) continue;
  169. const attractSnapInfos: AttractSnapInfo[] = [];
  170. for (const ref of refInfos) {
  171. let infos = filters(self, ref, attractSnapInfos);
  172. const stop = Array.isArray(infos) ? false : !!infos.stop;
  173. const stopRef = Array.isArray(infos) ? false : !!infos.stopSelf;
  174. infos = Array.isArray(infos) ? infos : infos.items!;
  175. if (infos && sortKey) {
  176. for (const info of infos) {
  177. arrayInsert(
  178. attractSnapInfos,
  179. info,
  180. (a, b) => b[sortKey] < a[sortKey]
  181. );
  182. }
  183. }
  184. if (stop) {
  185. return attractSnapInfosGroups;
  186. }
  187. if (stopRef) {
  188. break;
  189. }
  190. }
  191. arrayInsert(
  192. attractSnapInfosGroups,
  193. attractSnapInfos,
  194. (a, b) => a.length < b.length
  195. );
  196. }
  197. return attractSnapInfosGroups;
  198. };
  199. type FilterAttrib = {
  200. maxOffset?: number;
  201. maxAngle?: number;
  202. type?: "inter" | "projection" | "all" | "none";
  203. };
  204. const getAttractSnapInfos = (
  205. ref: ComponentSnapInfo,
  206. self: ComponentSnapInfo,
  207. filter: FilterAttrib
  208. ) => {
  209. filter.type = filter.type || "all";
  210. const limitAngle = "maxAngle" in filter;
  211. const limitOffset = "maxOffset" in filter;
  212. const isAll = filter.type === "all";
  213. const attractSnapInfos: AttractSnapInfo[] = [];
  214. for (let i = 0; i < self.linkAngle.length; i++) {
  215. for (let j = 0; j < ref.linkAngle.length; j++) {
  216. const angle = Math.abs(ref.linkAngle[j] - self.linkAngle[i]);
  217. if (limitAngle && angle > filter.maxAngle!) {
  218. continue;
  219. }
  220. let join: Pos | null = null;
  221. let offset: number | null = null;
  222. const checkOffset = (getJoin: () => Pos | null) => {
  223. join = getJoin();
  224. offset = join && lineLen(self.point, join);
  225. const adopt = join && (!limitOffset || offset! < filter.maxOffset!);
  226. if (!adopt) {
  227. join = null;
  228. offset = null;
  229. }
  230. return adopt;
  231. };
  232. if (filter.type === "none") {
  233. const adopt = checkOffset(() => ref.point);
  234. if (!adopt) continue;
  235. } else {
  236. const refLine = [
  237. ref.point,
  238. vector(ref.point).add(ref.linkDirections[j]),
  239. ];
  240. if (filter.type === "projection" || isAll) {
  241. const adopt = checkOffset(() =>
  242. linePointProjection(refLine, self.point)
  243. );
  244. if (!adopt && !isAll) continue;
  245. }
  246. const curLine = [
  247. self.point,
  248. vector(self.point).add(self.linkDirections[i]),
  249. ];
  250. if (!join && !checkOffset(() => lineIntersection(refLine, curLine))) {
  251. continue;
  252. }
  253. }
  254. attractSnapInfos.push({
  255. ref,
  256. current: self,
  257. refLinkNdx: j,
  258. currentLinkNdx: i,
  259. angle,
  260. join: join!,
  261. offset: offset!,
  262. refDirection: ref.linkDirections[j],
  263. });
  264. }
  265. }
  266. return attractSnapInfos;
  267. };
  268. const moveSnap = (
  269. refInfos: ComponentSnapInfo[],
  270. selfInfos: ComponentSnapInfo[],
  271. filter: FilterAttrib
  272. ) => {
  273. filter.maxOffset = filter.maxOffset || 15;
  274. const exclude = new Map<AttractSnapInfo, AttractSnapInfo>();
  275. const addExclude = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
  276. exclude.set(nor, act);
  277. exclude.set(act, nor);
  278. };
  279. const getAttractSnapJoin = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
  280. if (nor === act || exclude.get(nor) === act) {
  281. return void 0;
  282. }
  283. if (eqPoint(nor.current.point, act.join)) {
  284. return addExclude(nor, act);
  285. }
  286. // console.log(nor.current.point, act.join)
  287. const norJoin = nor.join;
  288. const norDire = vector(nor.refDirection);
  289. const norLine = createLine(norJoin, norDire);
  290. // TODO 确保移动前后normal的 direction 保持一致
  291. const useDire = act.refDirection;
  292. if (eqNGDire(norDire, useDire)) {
  293. return addExclude(nor, act);
  294. }
  295. const useJoin = act.join;
  296. const useLine = createLine(useJoin, useDire);
  297. const nuJoin = lineIntersection(norLine, useLine);
  298. if (!nuJoin || lineLen(nuJoin, norJoin) > filter.maxOffset!) {
  299. return addExclude(nor, act);
  300. } else {
  301. return nuJoin;
  302. }
  303. };
  304. const useAttractSnaps: AttractSnapInfo[] = [];
  305. let end: Pos | null = null;
  306. let start: Pos | null = null;
  307. // TODO 最多参考2个信息
  308. const attractSnapGroups = filterAttractSnapInfos(
  309. refInfos,
  310. selfInfos,
  311. (self, ref, selfGroup) => {
  312. const attractSnapInfos = getAttractSnapInfos(ref, self, filter);
  313. const nor = selfGroup[0] || attractSnapInfos[0];
  314. const checks = [...selfGroup, ...attractSnapInfos];
  315. // TODO 尽快提前结束
  316. for (const check of checks) {
  317. const join = getAttractSnapJoin(nor, check);
  318. if (join) {
  319. end = join;
  320. start = nor.current.point;
  321. useAttractSnaps.push(nor, check);
  322. return { stop: true };
  323. }
  324. }
  325. return attractSnapInfos;
  326. },
  327. "offset"
  328. );
  329. if (!end) {
  330. if (!attractSnapGroups.length || !attractSnapGroups[0].length) return null;
  331. const nor = attractSnapGroups[0][0];
  332. end = nor.join;
  333. start = nor.current.point;
  334. useAttractSnaps.push(nor);
  335. // TODO 如果没有同一个点的两线段,则使用2垂直的两点线段
  336. const move = vector(end!).sub(start!);
  337. for (let i = 0; i < attractSnapGroups.length; i++) {
  338. let j = i === 0 ? 1 : 0;
  339. for (; j < attractSnapGroups[i].length; j++) {
  340. const attractSnap = attractSnapGroups[i][j];
  341. const rDire = attractSnap.refDirection;
  342. const angle = vector2IncludedAngle(nor.refDirection, rDire);
  343. if (!numEq(rangMod(angle, Math.PI), Math.PI / 2)) {
  344. continue;
  345. }
  346. const cPoint = vector(attractSnap.current.point).add(move);
  347. const rPoint = attractSnap.ref.point;
  348. const inter = lineIntersection(
  349. createLine(cPoint, nor.refDirection),
  350. createLine(rPoint, rDire)
  351. );
  352. if (inter) {
  353. useAttractSnaps.push(attractSnap);
  354. end = vector(end).add(inter.sub(cPoint));
  355. break;
  356. }
  357. }
  358. if (j !== attractSnapGroups[i].length) break;
  359. }
  360. }
  361. const norMove = vector(end!).sub(start!);
  362. return {
  363. useAttractSnaps,
  364. transform: new Transform().translate(norMove.x, norMove.y),
  365. };
  366. };
  367. type SelfAttitude = {
  368. rotation: number;
  369. origin: Pos;
  370. operTarget: Pos;
  371. center: Pos;
  372. };
  373. const scaleSnap = (
  374. refInfos: ComponentSnapInfo[],
  375. selfInfos: ComponentSnapInfo[],
  376. filter: Omit<FilterAttrib, "type">,
  377. type: ScaleVectorType,
  378. attitude: SelfAttitude,
  379. testPoints: Ref<Pos[]>
  380. ) => {
  381. const { origin, operTarget } = attitude;
  382. const attractSnaps: AttractSnapInfo[] = [];
  383. const operVector = vector(operTarget).sub(origin).normalize();
  384. const vOperVector = verticalVector(operVector);
  385. const vLine = createLine(origin, vOperVector);
  386. const proportional = [
  387. "top-right",
  388. "top-left",
  389. "bottom-right",
  390. "bottom-left",
  391. ].includes(type);
  392. const limitOffset = filter.maxOffset;
  393. const asFilter: FilterAttrib = { ...filter, type: "none" };
  394. delete asFilter.maxOffset;
  395. const exclude = new Set<AttractSnapInfo>();
  396. const getAttractSnapJoin = (nor: AttractSnapInfo) => {
  397. if (exclude.has(nor)) return;
  398. if (
  399. eqNGDire(nor.refDirection, operVector) ||
  400. eqNGDire(
  401. nor.refDirection,
  402. nor.current.linkDirections[nor.currentLinkNdx]
  403. ) ||
  404. Math.abs(
  405. vector(origin).sub(nor.current.point).normalize().dot(operVector)
  406. ) < 0.01
  407. ) {
  408. exclude.add(nor);
  409. return;
  410. }
  411. const cur = nor.current.point;
  412. if (
  413. eqNGDire(vOperVector, nor.refDirection) &&
  414. zeroEq(linePointLen(vLine, nor.ref.point))
  415. ) {
  416. exclude.add(nor);
  417. attractSnaps.push(nor);
  418. return;
  419. }
  420. const refLine = [
  421. nor.ref.point,
  422. vector(nor.ref.point).add(nor.refDirection),
  423. ];
  424. const norLine = proportional
  425. ? [origin, cur]
  426. : [cur, vector(cur).add(operVector)];
  427. const join = lineIntersection(refLine, norLine);
  428. if (!join || (limitOffset && lineLen(join, cur) > limitOffset)) {
  429. exclude.add(nor);
  430. return;
  431. }
  432. // testPoints.value = [origin, nor.ref.point];
  433. return join;
  434. };
  435. let useAttractSnap: AttractSnapInfo;
  436. let useJoin: Pos;
  437. // TODO 最多参考1个信息
  438. filterAttractSnapInfos(refInfos, selfInfos, (self, ref) => {
  439. if (eqPoint(self.point, origin)) return { stopSelf: true };
  440. const attractSnapInfos = getAttractSnapInfos(ref, self, asFilter);
  441. for (const info of attractSnapInfos) {
  442. const join = getAttractSnapJoin(info);
  443. if (join) {
  444. info.join = join;
  445. useJoin = join;
  446. useAttractSnap = info;
  447. return { stop: true };
  448. }
  449. }
  450. return attractSnapInfos;
  451. });
  452. if (!useAttractSnap! || !useJoin!) return null;
  453. const rotation = Math.atan2(operVector.y, operVector.x);
  454. const invRotateTransform = new Transform()
  455. .rotate(-rotation)
  456. .translate(-origin.x, -origin.y);
  457. const t = invRotateTransform.point(useJoin!);
  458. const c = invRotateTransform.point(useAttractSnap.current.point);
  459. const currentFactor = vector({
  460. x: numEq(t.x, c.x) ? 1 : t.x / c.x,
  461. y: numEq(t.y, c.y) ? 1 : t.y / c.y,
  462. });
  463. if (proportional) {
  464. currentFactor.y = currentFactor.x;
  465. }
  466. attractSnaps.push(useAttractSnap);
  467. return {
  468. useAttractSnaps: attractSnaps,
  469. transform: new Transform()
  470. .multiply(invRotateTransform.copy().invert())
  471. .scale(currentFactor.x, currentFactor.y)
  472. .multiply(invRotateTransform),
  473. };
  474. };
  475. export const useSnap = (
  476. snapInfos: { value: ComponentSnapInfo[] } = useGlobalSnapInfos()
  477. ) => {
  478. const snapResultInfo = useSnapResultInfo();
  479. const snapConfig = useSnapConfig();
  480. const testPoints = useTestPoints();
  481. const afterHandler = (result: ReturnType<typeof moveSnap>) => {
  482. if (result) {
  483. snapResultInfo.attractSnaps = result.useAttractSnaps;
  484. return result.transform;
  485. }
  486. return null;
  487. };
  488. const move = (selfSnapInfos: ComponentSnapInfo[]) => {
  489. const result = moveSnap(snapInfos.value, selfSnapInfos, {
  490. maxOffset: snapConfig.snapOffset,
  491. });
  492. snapResultInfo.selfSnaps.push(...selfSnapInfos);
  493. return afterHandler(result);
  494. };
  495. const scale = (
  496. selfSnapInfos: ComponentSnapInfo[],
  497. attitude: SelfAttitude & { type: ScaleVectorType }
  498. ) => {
  499. const result = scaleSnap(
  500. snapInfos.value,
  501. selfSnapInfos,
  502. { maxOffset: snapConfig.snapOffset },
  503. attitude.type,
  504. attitude,
  505. testPoints
  506. );
  507. snapResultInfo.selfSnaps = selfSnapInfos;
  508. return afterHandler(result);
  509. };
  510. return {
  511. move,
  512. scale,
  513. clear: snapResultInfo.clear,
  514. };
  515. };
  516. export const useComponentSnap = (componentId: string) => {
  517. const store = useStore();
  518. const type = componentId ? store.getType(componentId) : undefined;
  519. const comp = type && components[type];
  520. const api = type && comp?.getSnapInfos;
  521. if (!api) return null;
  522. const snapInfos = useGlobalSnapInfos();
  523. const refSnapInfos = computed(() =>
  524. snapInfos.value.filter((p) => !("id" in p) || p.id !== componentId)
  525. );
  526. const baseSnap = useSnap(refSnapInfos);
  527. const getOperType = useGetTransformerOperType();
  528. const getTransformerOperDirection = useGetTransformerOperDirection();
  529. const transformer = useTransformer();
  530. const viewerInvertTransform = useViewerInvertTransform();
  531. const snap = (item: BaseItem) => {
  532. const operateType = getOperType();
  533. const selfSnapInfos = api(item as any);
  534. baseSnap.clear();
  535. // move
  536. if (!operateType) {
  537. return baseSnap.move(selfSnapInfos);
  538. } else if (operateType !== "rotater") {
  539. const direction = getTransformerOperDirection()!;
  540. const node = transformer.nodes()[0];
  541. const rect = node.getClientRect();
  542. const attitude = {
  543. center: viewerInvertTransform.value.point({
  544. x: rect.x + rect.width / 2,
  545. y: rect.y + rect.height / 2,
  546. }),
  547. operTarget: direction[1],
  548. rotation: MathUtils.degToRad(node.rotation()),
  549. origin: direction[0],
  550. type: operateType,
  551. };
  552. return baseSnap.scale(selfSnapInfos, attitude);
  553. }
  554. };
  555. return [snap, baseSnap.clear] as const;
  556. };