use-interactive.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import { installGlobalVar, useStage } from "./use-global-vars.ts";
  2. import { useCan, useMode } from "./use-status";
  3. import { DrawItem, ShapeType } from "../components";
  4. import { nextTick, reactive, Ref, ref, watch, watchEffect } from "vue";
  5. import { eqPoint, Pos } from "../../utils/math.ts";
  6. import { clickListener, getOffset, listener } from "../../utils/event.ts";
  7. import { mergeFuns } from "../../utils/shared.ts";
  8. import { Mode } from "@/constant/mode.ts";
  9. import { useMouseShapesStatus } from "./use-mouse-status.ts";
  10. import { EntityShape } from "@/deconstruction.js";
  11. export type InteractivePreset<T extends ShapeType = ShapeType> = {
  12. key?: string;
  13. type: T;
  14. callback?: () => void;
  15. preset?: Partial<DrawItem<T>> & { getMessages?: () => Pos[] | Area[] };
  16. operate?: {
  17. immediate?: boolean;
  18. single?: boolean;
  19. data?: any;
  20. preSelectIds?: string[];
  21. };
  22. };
  23. export const useInteractiveProps = installGlobalVar(() => {
  24. const props = ref<InteractivePreset | undefined>();
  25. return props;
  26. }, Symbol("interactiveProps"));
  27. export type Area = [Pos, Pos];
  28. export type InteractiveHook =
  29. | typeof useInteractiveAreas
  30. | typeof useInteractiveDots;
  31. export type InteractiveAreas = ReturnType<typeof useInteractiveAreas>;
  32. export type InteractiveDots = ReturnType<typeof useInteractiveDots>;
  33. export type Interactive = InteractiveAreas | InteractiveDots;
  34. const useInteractiveExpose = <T extends object>(
  35. messages: Ref<T[]>,
  36. init: (dom: HTMLDivElement) => () => void,
  37. singleDone: Ref<boolean>,
  38. isRunning: Ref<boolean>,
  39. quit: () => void,
  40. autoConsumed?: boolean,
  41. attachInfos?: WeakMap<T, any>,
  42. ) => {
  43. const consumedMessages = reactive(new WeakSet<T>()) as WeakSet<T>;
  44. const stage = useStage();
  45. const interactiveProps = useInteractiveProps();
  46. watch(
  47. isRunning,
  48. (can, _, onCleanup) => {
  49. if (can) {
  50. const props = interactiveProps.value!;
  51. const cleanups = [] as Array<() => void>;
  52. if (props.operate?.single) {
  53. // 如果指定单次则消息中有信息,并且确定完成则马上退出
  54. cleanups.push(
  55. watchEffect(
  56. () => {
  57. if (messages.value.length > 0 && singleDone.value) {
  58. quit();
  59. props.callback && props.callback();
  60. }
  61. },
  62. { flush: "post" },
  63. ),
  64. );
  65. }
  66. // 单纯添加
  67. if (props.operate?.immediate) {
  68. messages.value.push(props.operate.data as T);
  69. singleDone.value = true;
  70. } else {
  71. const $stage = stage.value!.getStage();
  72. const dom = $stage.container();
  73. cleanups.push(init(dom));
  74. cleanups.push(() => {
  75. quit();
  76. props.callback && props.callback();
  77. });
  78. }
  79. onCleanup(mergeFuns(cleanups));
  80. } else {
  81. messages.value = [];
  82. }
  83. },
  84. { immediate: true },
  85. );
  86. return {
  87. attachInfos,
  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. enter?: () => void;
  116. shapeType?: ShapeType;
  117. autoConsumed?: boolean;
  118. };
  119. export const useInteractiveAreas = ({
  120. isRuning,
  121. autoConsumed,
  122. beforeHandler,
  123. quit,
  124. enter,
  125. }: UseInteractiveProps) => {
  126. const mode = useMode();
  127. const can = useCan();
  128. const singleDone = ref(true);
  129. const messages = ref<Area[]>([]);
  130. const init = (dom: HTMLDivElement) => {
  131. let pushed = false;
  132. let pushNdx = -1;
  133. let downed = false;
  134. let tempArea: Area;
  135. let dragging = false;
  136. enter && enter();
  137. const upHandler = (ev: MouseEvent) => {
  138. if (downed) {
  139. mode.del(Mode.draging);
  140. }
  141. downed = false;
  142. if (!dragging) return;
  143. if (can.dragMode) {
  144. const position = getOffset(ev, dom);
  145. messages.value[pushNdx]![1] = beforeHandler
  146. ? beforeHandler(position)
  147. : position;
  148. }
  149. prevEv = null;
  150. pushNdx = -1;
  151. pushed = false;
  152. downed = false;
  153. dragging = false;
  154. singleDone.value = true;
  155. };
  156. let prevEv: any;
  157. return mergeFuns(
  158. listener(dom, "pointerdown", (ev) => {
  159. if (!can.dragMode) return;
  160. const position = getOffset(ev, dom);
  161. if (ev.button === 0) {
  162. tempArea = [
  163. beforeHandler ? beforeHandler(position) : position,
  164. ] as unknown as Area;
  165. downed = true;
  166. singleDone.value = false;
  167. dragging = false;
  168. mode.add(Mode.draging);
  169. }
  170. }),
  171. listener(document.documentElement, "pointermove", (ev) => {
  172. if (!can.dragMode) return;
  173. if (ev.buttons <= 0) {
  174. prevEv && upHandler(prevEv);
  175. return;
  176. }
  177. const end = getOffset(ev, dom);
  178. const point = beforeHandler ? beforeHandler(end) : end;
  179. prevEv = ev;
  180. if (downed) {
  181. if (pushed) {
  182. messages.value[pushNdx]![1] = point;
  183. } else {
  184. tempArea[1] = point;
  185. pushed = true;
  186. pushNdx = messages.value.length;
  187. messages.value[pushNdx] = tempArea;
  188. }
  189. dragging = true;
  190. } else {
  191. tempArea = [point] as unknown as Area;
  192. }
  193. }),
  194. listener(document.documentElement, "pointerup", upHandler),
  195. );
  196. };
  197. return useInteractiveExpose(
  198. messages,
  199. init,
  200. singleDone,
  201. isRuning,
  202. quit,
  203. autoConsumed,
  204. );
  205. };
  206. export const useInteractiveDots = ({
  207. autoConsumed,
  208. isRuning,
  209. quit,
  210. enter,
  211. beforeHandler,
  212. }: UseInteractiveProps) => {
  213. if (autoConsumed === void 0) autoConsumed = false;
  214. const interactiveProps = useInteractiveProps();
  215. const attachInfos = new WeakMap<Pos, any>();
  216. const mode = useMode();
  217. const can = useCan();
  218. const singleDone = ref(true);
  219. const messages = ref<Pos[]>([]);
  220. const stage = useStage();
  221. const shapesStatus = useMouseShapesStatus();
  222. const init = (dom: HTMLDivElement) => {
  223. if (!can.dragMode) return () => {};
  224. let moveIng = false;
  225. let pushed = false;
  226. const empty = { x: -9999, y: -9999 };
  227. const pointer = ref(empty);
  228. const beforePointer = ref(empty);
  229. enter && enter();
  230. mode.add(Mode.draging);
  231. const posMove = (position: Pos) => {
  232. if (!can.dragMode) return;
  233. if (!pushed) {
  234. messages.value.push(pointer.value);
  235. singleDone.value = false;
  236. pushed = true;
  237. }
  238. moveIng = true;
  239. const current = beforeHandler ? beforeHandler(position) : position;
  240. pointer.value.x = current.x;
  241. pointer.value.y = current.y;
  242. beforePointer.value = { ...position };
  243. };
  244. const move = (ev: MouseEvent) => {
  245. posMove(getOffset(ev));
  246. };
  247. const preSelectIds = interactiveProps.value?.operate?.preSelectIds;
  248. let prevPoint: Pos = { ...empty };
  249. const $stage = stage.value!.getNode();
  250. if (preSelectIds) {
  251. shapesStatus.actives = preSelectIds
  252. .map((id) => $stage.findOne(`#${id}`))
  253. .filter(Boolean) as EntityShape[];
  254. }
  255. return mergeFuns(
  256. () => {
  257. mode.del(Mode.draging);
  258. shapesStatus.actives = []
  259. },
  260. watch(singleDone, () => {
  261. if (singleDone.value) {
  262. prevPoint = pointer.value;
  263. const prevBeforePoint = beforePointer.value;
  264. pointer.value = { ...empty };
  265. beforePointer.value = { ...empty };
  266. singleDone.value = true;
  267. moveIng = false;
  268. pushed = false;
  269. nextTick(() => posMove(prevBeforePoint));
  270. }
  271. }),
  272. clickListener(dom, (_, ev) => {
  273. if (!moveIng || !can.dragMode || eqPoint(prevPoint, pointer.value))
  274. return;
  275. if (preSelectIds?.length && $stage) {
  276. const joinIds: string[] = [];
  277. for (const id of preSelectIds) {
  278. const $shape = $stage.findOne(`#${id}`);
  279. if (!$shape) continue;
  280. const rect = $shape.getClientRect();
  281. let x = ev.offsetX,
  282. y = ev.offsetY;
  283. if (
  284. x > rect.x &&
  285. x < rect.x + rect.width &&
  286. y > rect.y &&
  287. y < rect.y + rect.height
  288. ) {
  289. joinIds.push(id);
  290. }
  291. }
  292. if (joinIds.length) {
  293. attachInfos.set(pointer.value, joinIds);
  294. return;
  295. }
  296. }
  297. singleDone.value = true;
  298. }),
  299. listener(dom, "pointermove", move),
  300. );
  301. };
  302. return useInteractiveExpose(
  303. messages,
  304. init,
  305. singleDone,
  306. isRuning,
  307. quit,
  308. autoConsumed,
  309. attachInfos,
  310. );
  311. };