use-add.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import { h, nextTick, reactive, ref, watch, watchEffect } from "vue";
  2. import {
  3. installGlobalVar,
  4. useCan,
  5. useCursor,
  6. useDownKeys,
  7. useMode,
  8. useStage,
  9. } from "./use-global-vars";
  10. import {
  11. Area,
  12. InteractiveHook,
  13. useInteractiveAreas,
  14. useInteractiveDots,
  15. useInteractiveProps,
  16. } from "./use-interactive";
  17. import { Mode } from "@/constant/mode";
  18. import { mergeFuns } from "@/utils/shared";
  19. import {
  20. Components,
  21. components,
  22. ComponentSnapInfo,
  23. ComponentValue,
  24. DrawItem,
  25. ShapeType,
  26. SnapPoint,
  27. } from "../components";
  28. import { useConversionPosition } from "./use-coversion-position";
  29. import { eqPoint, lineInner, linePointLen, Pos, zeroEq } from "@/utils/math";
  30. import { useCustomSnapInfos, useSnap } from "./use-snap";
  31. import { generateSnapInfos } from "../components/util";
  32. import { useStore, useStoreRenderProcessors } from "../store";
  33. import DrawShape from "../renderer/draw-shape.vue";
  34. import { useHistory, useHistoryAttach } from "./use-history";
  35. import penA from "../assert/cursor/pic_pen_a.ico";
  36. import penR from "../assert/cursor/pic_pen_r.ico";
  37. type PayData<T extends ShapeType> = ComponentValue<T, "addMode"> extends "area"
  38. ? Area
  39. : Pos;
  40. export enum MessageAction {
  41. add,
  42. delete,
  43. update,
  44. replace,
  45. }
  46. export type AddMessage<T extends ShapeType> = {
  47. consumed: PayData<T>[];
  48. cur?: PayData<T>;
  49. ndx?: number;
  50. action: MessageAction;
  51. };
  52. export const useInteractiveAddShapeAPI = installGlobalVar(() => {
  53. const mode = useMode();
  54. const can = useCan();
  55. const interactiveProps = useInteractiveProps();
  56. const conversion = useConversionPosition(true);
  57. let isEnter = false;
  58. const enter = () => {
  59. if (!isEnter) {
  60. isEnter = true;
  61. mode.push(Mode.add);
  62. }
  63. };
  64. const leave = () => {
  65. if (isEnter) {
  66. isEnter = false;
  67. mode.pop();
  68. }
  69. };
  70. return {
  71. addShape: <T extends ShapeType>(
  72. shapeType: T,
  73. preset: Partial<DrawItem<T>> = {},
  74. data: PayData<T>,
  75. pixel = false
  76. ) => {
  77. if (!can.addMode) {
  78. throw "当前状态不允许添加";
  79. }
  80. enter();
  81. if (pixel) {
  82. data = (
  83. Array.isArray(data) ? data.map(conversion) : conversion(data)
  84. ) as PayData<T>;
  85. }
  86. interactiveProps.value = {
  87. type: shapeType,
  88. preset,
  89. callback: leave,
  90. operate: { single: true, immediate: true, data },
  91. };
  92. },
  93. enterMouseAddShape: async <T extends ShapeType>(
  94. shapeType: T,
  95. preset: Partial<DrawItem<T>> = {},
  96. single = false
  97. ) => {
  98. if (isEnter) {
  99. leave();
  100. await new Promise((resolve) => setTimeout(resolve, 16));
  101. }
  102. if (!can.addMode || mode.include(Mode.add)) {
  103. throw "当前状态不允许添加";
  104. }
  105. enter();
  106. interactiveProps.value = {
  107. type: shapeType,
  108. preset,
  109. operate: { single },
  110. callback: leave,
  111. };
  112. },
  113. quitMouseAddShape: () => {
  114. leave();
  115. interactiveProps.value = void 0;
  116. },
  117. };
  118. });
  119. export const useIsAddRunning = (shapeType?: ShapeType) => {
  120. const stage = useStage();
  121. const mode = useMode();
  122. const interactiveProps = useInteractiveProps();
  123. const isRunning = ref<boolean>(false);
  124. let currentPreset: any;
  125. const updateIsRunning = () => {
  126. const isRun = !!(
  127. stage.value &&
  128. mode.include(Mode.add) &&
  129. (!shapeType || shapeType === interactiveProps.value?.type)
  130. );
  131. if (isRunning.value !== isRun) {
  132. isRunning.value = isRun;
  133. } else if (currentPreset !== interactiveProps.value?.preset) {
  134. isRunning.value = false;
  135. nextTick(() => {
  136. isRunning.value = isRun;
  137. });
  138. }
  139. currentPreset = interactiveProps.value?.preset;
  140. };
  141. watchEffect(updateIsRunning);
  142. return isRunning;
  143. };
  144. const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
  145. const conversionPosition = useConversionPosition(enableTransform);
  146. const snap = enableSnap && useSnap();
  147. const infos = useCustomSnapInfos();
  148. const addedInfos: ComponentSnapInfo[] = [];
  149. return {
  150. transform: (
  151. point: SnapPoint,
  152. prevPoint?: SnapPoint,
  153. nextPoint?: SnapPoint
  154. ) => {
  155. snap && snap.clear();
  156. let p = conversionPosition(point);
  157. const geo = [p];
  158. prevPoint && geo.unshift({ ...prevPoint, view: true });
  159. nextPoint && geo.push({ ...nextPoint, view: true });
  160. const selfInfos = generateSnapInfos(geo, true, true);
  161. const transform = snap && snap.move(selfInfos);
  162. p = transform ? transform.point(p) : p;
  163. return p;
  164. },
  165. addRef(p: Pos | Pos[]) {
  166. const geo = Array.isArray(p) ? p : [p];
  167. const snapInfos = generateSnapInfos(geo, true, true);
  168. snapInfos.forEach((info) => {
  169. infos.add(info);
  170. addedInfos.push(info);
  171. });
  172. },
  173. clear: () => {
  174. snap && snap.clear();
  175. },
  176. clearRef: () => {
  177. addedInfos.forEach((info) => {
  178. infos.remove(info);
  179. });
  180. addedInfos.length = 0;
  181. },
  182. };
  183. };
  184. const useInteractiveAddTemp = <T extends ShapeType>({
  185. type,
  186. useIA,
  187. refSelf,
  188. enter,
  189. quit,
  190. }: {
  191. type: T;
  192. useIA: InteractiveHook;
  193. refSelf?: boolean;
  194. enter?: () => void;
  195. quit?: () => void;
  196. }) => {
  197. const { quitMouseAddShape } = useInteractiveAddShapeAPI();
  198. const isRuning = useIsAddRunning(type);
  199. const items = reactive([]) as DrawItem<T>[];
  200. const obj = components[type] as Components[T];
  201. const beforeHandler = usePointBeforeHandler(true, true);
  202. const processors = useStoreRenderProcessors();
  203. const store = useStore();
  204. const processorIds = processors.register(() => DrawShape);
  205. const clear = () => {
  206. beforeHandler.clear();
  207. beforeHandler.clearRef();
  208. };
  209. const ia = useIA({
  210. shapeType: type,
  211. isRuning,
  212. quit: () => {
  213. items.length = 0;
  214. processorIds.length = 0;
  215. quitMouseAddShape();
  216. clear();
  217. quit && quit();
  218. },
  219. enter,
  220. beforeHandler: (p) => {
  221. beforeHandler.clear();
  222. return beforeHandler.transform(p);
  223. },
  224. });
  225. const addItem = (cur: PayData<T>) => {
  226. let item: any = obj.interactiveToData(
  227. {
  228. consumed: ia.consumedMessage,
  229. cur,
  230. action: MessageAction.add,
  231. } as any,
  232. ia.preset
  233. );
  234. if (!item) return;
  235. item = reactive(item);
  236. if (ia.singleDone.value) {
  237. store.addItem(type, item);
  238. return;
  239. }
  240. items.push(item);
  241. // 箭头参考自身位置
  242. if (refSelf && Array.isArray(cur)) {
  243. beforeHandler.addRef(cur[0]);
  244. }
  245. const stop = mergeFuns(
  246. // 监听位置变化
  247. watch(
  248. cur,
  249. () =>
  250. obj.interactiveFixData(item, {
  251. consumed: ia.consumedMessage,
  252. cur,
  253. action: MessageAction.update,
  254. } as any),
  255. { deep: true }
  256. ),
  257. // 监听是否消费完毕
  258. watch(ia.singleDone, () => {
  259. processorIds.push(item.id);
  260. store.addItem(type, item);
  261. const ndx = items.indexOf(item);
  262. items.splice(ndx, 1);
  263. clear();
  264. stop();
  265. })
  266. );
  267. };
  268. // 每次拽结束都加组件
  269. watch(
  270. () => ia.messages,
  271. (datas: any) => {
  272. datas.forEach(addItem);
  273. ia.consume(datas);
  274. },
  275. { immediate: true }
  276. );
  277. return items;
  278. };
  279. // 拖拽面积确定组件
  280. export const useInteractiveAddAreas = <T extends ShapeType>(type: T) => {
  281. const cursor = useCursor();
  282. return useInteractiveAddTemp({
  283. type,
  284. useIA: useInteractiveAreas,
  285. refSelf: type === "arrow",
  286. enter() {
  287. cursor.push("crosshair");
  288. },
  289. quit() {
  290. cursor.pop();
  291. },
  292. });
  293. };
  294. export const useInteractiveAddDots = <T extends ShapeType>(type: T) => {
  295. const cursor = useCursor();
  296. return useInteractiveAddTemp({
  297. type,
  298. useIA: useInteractiveDots,
  299. enter() {
  300. cursor.push(penA);
  301. },
  302. quit() {
  303. cursor.pop();
  304. },
  305. });
  306. };
  307. export const penUpdatePoints = <T extends Pos>(
  308. transfromPoints: T[],
  309. cur: T
  310. ) => {
  311. const points = [...transfromPoints];
  312. let oper: "del" | "add" | "set" | "no" = "add";
  313. const resetCur = () => {
  314. if (points.length) {
  315. return (cur = points.pop()!);
  316. } else {
  317. return cur;
  318. }
  319. };
  320. let repeatStart = false;
  321. for (let i = 0; i < points.length; i++) {
  322. if (eqPoint(points[i], cur)) {
  323. const isLast = i === points.length - 1;
  324. const isStart = i === 0;
  325. if (!isStart && !isLast) {
  326. points.splice(i--, 1);
  327. oper = "del";
  328. } else if ((oper !== "del" && isLast) || isStart) {
  329. oper = "no";
  330. if (isStart) {
  331. repeatStart = true;
  332. }
  333. }
  334. }
  335. }
  336. if (oper === "del" || oper === "no") {
  337. if (repeatStart) {
  338. const change = points.length > 2
  339. return {
  340. points,
  341. oper,
  342. cur: change ? cur : resetCur(),
  343. unchanged: !change
  344. };
  345. }
  346. return { points, oper, cur: repeatStart ? cur : resetCur() };
  347. }
  348. for (let i = 0, ndx = 0; i < transfromPoints.length - 1; i++, ndx++) {
  349. const line = [transfromPoints[i], transfromPoints[i + 1]];
  350. if (lineInner(line, cur)) {
  351. oper = "set";
  352. points.splice(++ndx, 0, cur);
  353. resetCur();
  354. }
  355. }
  356. return { points, oper, cur };
  357. };
  358. // 钢笔添加
  359. export const useInteractiveAddPen = <T extends ShapeType>(type: T) => {
  360. const { quitMouseAddShape } = useInteractiveAddShapeAPI();
  361. const isRuning = useIsAddRunning(type);
  362. const items = reactive([]) as DrawItem<T>[];
  363. const obj = components[type] as Components[T];
  364. const beforeHandler = usePointBeforeHandler(true, true);
  365. const history = useHistory();
  366. const processors = useStoreRenderProcessors();
  367. const store = useStore();
  368. const downKeys = useDownKeys();
  369. const processorIds = processors.register(() => {
  370. return (props: any) => h(DrawShape, { ...props, show: false });
  371. });
  372. let prev: Pos;
  373. let firstEntry = true;
  374. // 可能历史空间会撤销 重做更改到正在绘制的组件
  375. const messages = useHistoryAttach<Pos[]>(`${type}-pen`, isRuning, []);
  376. const currentCursor = ref(penA);
  377. const cursor = useCursor();
  378. const ia = useInteractiveDots({
  379. shapeType: type,
  380. isRuning,
  381. enter() {
  382. cursor.push(currentCursor.value);
  383. watch(currentCursor, () => {
  384. cursor.value = currentCursor.value;
  385. });
  386. },
  387. quit: () => {
  388. items.length = 0;
  389. processorIds.length = 0;
  390. quitMouseAddShape();
  391. beforeHandler.clear();
  392. cursor.pop();
  393. },
  394. beforeHandler: (p) => {
  395. beforeHandler.clear();
  396. return beforeHandler.transform(p, prev);
  397. },
  398. });
  399. const getAddMessage = (cur: Pos) => {
  400. let consumed = messages.value;
  401. currentCursor.value = penA;
  402. let pen: null | ReturnType<typeof penUpdatePoints> = null;
  403. if (!downKeys.has("Control")) {
  404. pen = penUpdatePoints(messages.value, cur);
  405. if (pen.oper === "del") {
  406. currentCursor.value = penR;
  407. }
  408. consumed = pen.points;
  409. cur = pen.cur;
  410. }
  411. return {
  412. pen,
  413. consumed,
  414. cur,
  415. action: firstEntry ? MessageAction.add : MessageAction.replace,
  416. } as any;
  417. };
  418. const pushMessages = (cur: Pos) => {
  419. const { pen } = getAddMessage(cur);
  420. if (pen) {
  421. if (!pen.unchanged) {
  422. messages.value = pen.points;
  423. cur = pen.cur;
  424. messages.value.push(cur);
  425. }
  426. } else {
  427. messages.value.push(cur);
  428. }
  429. return !pen?.unchanged;
  430. };
  431. const addItem = (cur: PayData<T>) => {
  432. const dot = cur as Pos;
  433. if (messages.value.length === 0) {
  434. firstEntry = true;
  435. items.length = 0;
  436. }
  437. let item: any = items.length === 0 ? null : items[0];
  438. if (!item) {
  439. item = obj.interactiveToData(getAddMessage(dot), ia.preset);
  440. if (!item) return;
  441. items[0] = item = reactive(item);
  442. }
  443. if (ia.singleDone.value) {
  444. store.addItem(type, item);
  445. return;
  446. }
  447. const update = () => {
  448. obj.interactiveFixData(item, getAddMessage(dot));
  449. };
  450. const stop = mergeFuns(
  451. watch(dot, update, { immediate: true, deep: true }),
  452. watch(
  453. messages,
  454. () => {
  455. if (!messages.value) return;
  456. if (messages.value.length === 0) {
  457. quitMouseAddShape();
  458. } else {
  459. update();
  460. }
  461. },
  462. { deep: true }
  463. ),
  464. // 监听是否消费完毕
  465. watch(ia.singleDone, () => {
  466. prev = dot;
  467. const cItem = JSON.parse(JSON.stringify(item));
  468. const isChange = pushMessages(dot);
  469. if (isChange) {
  470. if (firstEntry) {
  471. processorIds.push(item.id);
  472. history.preventTrack(() => store.addItem(type, cItem));
  473. } else {
  474. store.setItem(type, { id: item.id, value: cItem });
  475. }
  476. }
  477. beforeHandler.clear();
  478. stop();
  479. firstEntry = false;
  480. })
  481. );
  482. };
  483. // 每次拽结束都加组件
  484. watch(
  485. () => ia.messages,
  486. (datas: any) => {
  487. datas.forEach(addItem);
  488. ia.consume(datas);
  489. },
  490. { immediate: true }
  491. );
  492. return items;
  493. };
  494. export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
  495. const obj = components[type];
  496. if (obj.addMode === "dots") {
  497. return useInteractiveAddPen(type);
  498. } else if (obj.addMode === "area") {
  499. return useInteractiveAddAreas(type);
  500. } else {
  501. return useInteractiveAddDots(type);
  502. }
  503. };