use-draw.ts 15 KB

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