use-draw.ts 17 KB

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