use-interactive.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {
  2. installGlobalVar,
  3. useCan,
  4. useMode,
  5. useStage,
  6. } from "./use-global-vars.ts";
  7. import { DrawItem, ShapeType } from "../components";
  8. import { reactive, Ref, ref, watch, watchEffect } from "vue";
  9. import { Pos } from "../../utils/math.ts";
  10. import { clickListener, getOffset, listener } from "../../utils/event.ts";
  11. import { mergeFuns } from "../../utils/shared.ts";
  12. import { Mode } from "@/constant/mode.ts";
  13. export type InteractivePreset<T extends ShapeType = ShapeType> = {
  14. type: T;
  15. callback?: () => void;
  16. preset?: Partial<DrawItem<T>>;
  17. operate?: {
  18. immediate?: boolean;
  19. single?: boolean;
  20. data?: any;
  21. };
  22. };
  23. export const useInteractiveProps = installGlobalVar(
  24. () => ref<InteractivePreset | undefined>(),
  25. Symbol("interactiveProps")
  26. );
  27. export type Area = [Pos, Pos];
  28. export enum InteractiveAction {
  29. delete,
  30. }
  31. export type InteractiveMessage = {
  32. area?: Area;
  33. dot?: Pos;
  34. ndx?: number;
  35. action?: InteractiveAction;
  36. };
  37. export type InteractiveAreas = ReturnType<typeof useInteractiveAreas>;
  38. export type InteractiveDots = ReturnType<typeof useInteractiveDots>;
  39. export type Interactive = InteractiveAreas | InteractiveDots;
  40. const useInteractiveExpose = <T extends object>(
  41. messages: Ref<T[]>,
  42. init: (dom: HTMLDivElement) => () => void,
  43. singleDone: Ref<boolean>,
  44. isRunning: Ref<boolean>,
  45. quit: () => void,
  46. autoConsumed?: boolean
  47. ) => {
  48. const consumedMessages = reactive(new WeakSet<T>()) as WeakSet<T>;
  49. const stage = useStage();
  50. const interactiveProps = useInteractiveProps();
  51. watch(isRunning, (can, _, onCleanup) => {
  52. if (can) {
  53. const props = interactiveProps.value!;
  54. const cleanups = [] as Array<() => void>;
  55. if (props.operate?.single) {
  56. // 如果指定单次则消息中有信息,并且确定完成则马上退出
  57. cleanups.push(
  58. watchEffect(
  59. () => {
  60. if (messages.value.length > 0 && singleDone.value) {
  61. quit();
  62. props.callback && props.callback();
  63. }
  64. },
  65. { flush: "post" }
  66. )
  67. );
  68. }
  69. // 单纯添加
  70. if (props.operate?.immediate) {
  71. messages.value.push(props.operate.data as T);
  72. singleDone.value = true;
  73. } else {
  74. const $stage = stage.value!.getStage();
  75. const dom = $stage.container();
  76. cleanups.push(init(dom));
  77. cleanups.push(() => {
  78. quit();
  79. props.callback && props.callback();
  80. });
  81. }
  82. onCleanup(mergeFuns(cleanups));
  83. } else {
  84. messages.value = [];
  85. }
  86. });
  87. return {
  88. isRunning,
  89. get preset() {
  90. return interactiveProps.value?.preset;
  91. },
  92. get messages() {
  93. const items = messages.value;
  94. const result = items.filter((item) => !consumedMessages.has(item));
  95. autoConsumed && result.forEach((item) => consumedMessages.add(item));
  96. return result as T[];
  97. },
  98. getNdx(item: T) {
  99. return messages.value.indexOf(item);
  100. },
  101. get consumedMessage() {
  102. const items = messages.value;
  103. return items.filter((item) => consumedMessages.has(item)) as T[];
  104. },
  105. consume(items: T[]) {
  106. items.forEach((item) => consumedMessages.add(item));
  107. },
  108. singleDone,
  109. };
  110. };
  111. type UseInteractiveProps = {
  112. isRuning: Ref<boolean>;
  113. quit: () => void;
  114. beforeHandler?: (p: Pos) => Pos;
  115. shapeType?: ShapeType;
  116. autoConsumed?: boolean;
  117. };
  118. export const useInteractiveAreas = ({
  119. isRuning,
  120. autoConsumed,
  121. beforeHandler,
  122. quit,
  123. }: UseInteractiveProps) => {
  124. const mode = useMode();
  125. const can = useCan();
  126. const singleDone = ref(true);
  127. const messages = ref<Area[]>([]);
  128. const init = (dom: HTMLDivElement) => {
  129. let pushed = false;
  130. let pushNdx = -1;
  131. let downed = false;
  132. let tempArea: Area;
  133. let dragging = false;
  134. return mergeFuns(
  135. listener(dom, "pointerdown", (ev) => {
  136. if (!can.dragMode) return;
  137. const position = getOffset(ev, dom);
  138. if (ev.button === 0) {
  139. tempArea = [
  140. beforeHandler ? beforeHandler(position) : position,
  141. ] as unknown as Area;
  142. downed = true;
  143. singleDone.value = false;
  144. dragging = false;
  145. mode.add(Mode.draging);
  146. }
  147. }),
  148. listener(document.documentElement, "pointermove", (ev) => {
  149. if (!can.dragMode) return;
  150. const end = getOffset(ev, dom);
  151. const point = beforeHandler ? beforeHandler(end) : end;
  152. if (downed) {
  153. if (pushed) {
  154. messages.value[pushNdx]![1] = point;
  155. } else {
  156. tempArea[1] = point;
  157. pushed = true;
  158. pushNdx = messages.value.length;
  159. messages.value[pushNdx] = tempArea;
  160. }
  161. dragging = true;
  162. } else {
  163. tempArea = [point] as unknown as Area;
  164. }
  165. }),
  166. listener(dom, "pointerup", (ev) => {
  167. if (downed) {
  168. mode.del(Mode.draging);
  169. } else if (!dragging) return;
  170. if (can.dragMode) {
  171. const position = getOffset(ev, dom);
  172. messages.value[pushNdx]![1] = beforeHandler
  173. ? beforeHandler(position)
  174. : position;
  175. }
  176. pushNdx = -1;
  177. pushed = false;
  178. downed = false;
  179. dragging = false;
  180. singleDone.value = true;
  181. })
  182. );
  183. };
  184. return useInteractiveExpose(
  185. messages,
  186. init,
  187. singleDone,
  188. isRuning,
  189. quit,
  190. autoConsumed
  191. );
  192. };
  193. export const useInteractiveDots = ({
  194. autoConsumed,
  195. isRuning,
  196. quit,
  197. beforeHandler,
  198. }: UseInteractiveProps) => {
  199. if (autoConsumed === void 0) autoConsumed = false;
  200. const mode = useMode();
  201. const can = useCan();
  202. const singleDone = ref(true);
  203. const messages = ref<Pos[]>([]);
  204. const init = (dom: HTMLDivElement) => {
  205. if (!can.dragMode) return () => {};
  206. let moveIng = false;
  207. let pushed = false;
  208. const empty = { x: -9999, y: -9999 };
  209. const pointer = ref(empty);
  210. mode.add(Mode.draging);
  211. return mergeFuns(
  212. () => {
  213. mode.del(Mode.draging);
  214. },
  215. clickListener(dom, (_, ev) => {
  216. if (!moveIng || !can.dragMode) return;
  217. pointer.value = { ...empty };
  218. singleDone.value = true;
  219. moveIng = false;
  220. pushed = false;
  221. }),
  222. listener(dom, "pointermove", (ev) => {
  223. if (!can.dragMode) return;
  224. if (!pushed) {
  225. messages.value.push(pointer.value);
  226. singleDone.value = false;
  227. pushed = true;
  228. }
  229. moveIng = true;
  230. const position = getOffset(ev);
  231. const current = beforeHandler ? beforeHandler(position) : position;
  232. pointer.value.x = current.x;
  233. pointer.value.y = current.y;
  234. })
  235. );
  236. };
  237. return useInteractiveExpose(
  238. messages,
  239. init,
  240. singleDone,
  241. isRuning,
  242. quit,
  243. autoConsumed
  244. );
  245. };