use-draw.ts 17 KB

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