use-mouse-status.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import { computed, reactive, ref, Ref, toRaw, watch, watchEffect } from "vue";
  2. import { DC, EntityShape } from "../../deconstruction";
  3. import { Shape } from "konva/lib/Shape";
  4. import {
  5. globalWatch,
  6. installGlobalVar,
  7. usePointerIntersections,
  8. usePointerPos,
  9. useStage,
  10. useTransformIngShapes,
  11. } from "./use-global-vars.ts";
  12. import { useCan, useOperMode } from "./use-status";
  13. import { Stage } from "konva/lib/Stage";
  14. import { listener } from "../../utils/event.ts";
  15. import { asyncTimeout, inRevise, mergeFuns } from "../../utils/shared.ts";
  16. import { ComponentValue, DrawItem, ShapeType } from "../components";
  17. import { shapeTreeContain, shapeTreeContains } from "../../utils/shape.ts";
  18. import {
  19. usePointerIsTransformerInner,
  20. useTransformer,
  21. } from "./use-transformer.ts";
  22. import { KonvaEventObject } from "konva/lib/Node";
  23. import { useStore } from "../store/index.ts";
  24. import { Group } from "konva/lib/Group";
  25. import { usePause } from "./use-pause.ts";
  26. import { lineLen, Pos } from "@/utils/math.ts";
  27. import { useFormalLayer } from "./use-layer.ts";
  28. const stageHoverMap = new WeakMap<
  29. Stage,
  30. { result: Ref<EntityShape | undefined>; count: number; des: () => void }
  31. >();
  32. export const getHoverShape = (stage: Stage) => {
  33. let isStop = false;
  34. const stop = () => {
  35. if (isStop || !stageHoverMap.has(stage)) return;
  36. isStop = true;
  37. const data = stageHoverMap.get(stage)!;
  38. if (--data.count <= 0) {
  39. data.des();
  40. }
  41. };
  42. if (stageHoverMap.has(stage)) {
  43. const data = stageHoverMap.get(stage)!;
  44. ++data.count;
  45. return [data.result, stop] as const;
  46. }
  47. const hover = ref<EntityShape>();
  48. const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
  49. const target = ev.target;
  50. hover.value = target;
  51. target.off(`pointerleave`, leaveHandler);
  52. target.on(`pointerleave`, leaveHandler as any);
  53. };
  54. const leaveHandler = (ev?: KonvaEventObject<any, Stage>) => {
  55. if (hover.value) {
  56. hover.value.off(`pointerleave`, leaveHandler);
  57. hover.value = undefined;
  58. }
  59. };
  60. stage.on(`pointerenter`, enterHandler);
  61. stageHoverMap.set(stage, {
  62. result: hover,
  63. count: 1,
  64. des: () => {
  65. stage.off(`pointerenter`, enterHandler);
  66. leaveHandler();
  67. stageHoverMap.delete(stage);
  68. },
  69. });
  70. return [hover, stop] as const;
  71. };
  72. export const useShapeClick = (
  73. shape: Ref<DC<Shape> | undefined>,
  74. fn: () => void
  75. ) => {
  76. const init = (shape: Shape) => {
  77. let downTime: number;
  78. let move = false;
  79. const downHandler = (ev: KonvaEventObject<any>) => {
  80. if (ev.evt.button === 0) {
  81. ev.cancelBubble = true
  82. downTime = Date.now();
  83. move = false;
  84. }
  85. };
  86. const moveHandler = (ev: KonvaEventObject<any>) => {
  87. ev.cancelBubble = true
  88. downTime = Date.now();
  89. move = true;
  90. };
  91. const upHandler = (ev: KonvaEventObject<any>) => {
  92. ev.cancelBubble = true
  93. if (Date.now() - downTime < 500 && !move) {
  94. fn();
  95. }
  96. };
  97. shape.on("pointerdown.click", downHandler);
  98. shape.on("pointermove.click", moveHandler);
  99. shape.on("pointerup.click", upHandler);
  100. return () => {
  101. shape.off("pointerdown.click", downHandler);
  102. shape.off("pointermove.click", moveHandler);
  103. shape.off("pointerup.click", upHandler);
  104. };
  105. };
  106. watch(shape, (shape, _, onCleanup) => {
  107. const $shape = shape?.getNode();
  108. $shape && onCleanup(init($shape));
  109. });
  110. };
  111. export const useShapeIsHover = (shape: Ref<DC<EntityShape> | undefined>) => {
  112. const stage = useStage();
  113. const store = useStore();
  114. const format = useFormalLayer();
  115. const pos = usePointerPos();
  116. const isHover = ref(false);
  117. const stop = watch(
  118. () => ({ stage: stage.value?.getNode(), shape: shape.value?.getNode() }),
  119. ({ stage, shape }, _, onCleanup) => {
  120. if (!stage || !shape || result.isPause) {
  121. isHover.value = false;
  122. return;
  123. }
  124. const forciblyCheck = async () => {
  125. await asyncTimeout(6)
  126. if (!pos.value || !format.value) {
  127. isHover.value = false;
  128. return;
  129. }
  130. isHover.value =
  131. format.value.getIntersection(pos.value) === toRaw(shape);
  132. };
  133. store.bus.on("addItemAfter", forciblyCheck);
  134. store.bus.on("setItemAfter", forciblyCheck);
  135. store.bus.on("delItemAfter", forciblyCheck);
  136. const [hoverShape, stopHoverListener] = getHoverShape(stage);
  137. watchEffect(() => {
  138. isHover.value = !!(
  139. hoverShape.value && shapeTreeContain([shape], toRaw(hoverShape.value))
  140. );
  141. });
  142. onCleanup(
  143. mergeFuns([
  144. stopHoverListener,
  145. () => {
  146. isHover.value = false;
  147. store.bus.off("addItemAfter", forciblyCheck);
  148. store.bus.off("setItemAfter", forciblyCheck);
  149. store.bus.off("delItemAfter", forciblyCheck);
  150. },
  151. ])
  152. );
  153. },
  154. { immediate: true }
  155. );
  156. const result = usePause([isHover, stop] as const);
  157. return result;
  158. };
  159. export const useMouseShapeIsHover = (
  160. shape: Ref<DC<EntityShape> | undefined>
  161. ) => {
  162. const stage = useStage();
  163. const hitShapes = usePointerIntersections();
  164. const isHover = ref(false);
  165. const stop = watch(
  166. () => ({ stage: stage.value?.getNode(), shape: shape.value?.getNode() }),
  167. ({ stage, shape }, _, onCleanup) => {
  168. if (!stage || !shape || result.isPause) {
  169. isHover.value = false;
  170. return;
  171. }
  172. const stopHoverListener = watch(hitShapes, () => {
  173. isHover.value = hitShapes.value.includes(shape as any);
  174. });
  175. onCleanup(mergeFuns([stopHoverListener, () => (isHover.value = false)]));
  176. },
  177. { immediate: true }
  178. );
  179. const result = usePause([isHover, stop] as const);
  180. return result;
  181. };
  182. export const useShapeIsTransformerInner = () => {
  183. const transformer = useTransformer();
  184. const pointerIsTransformerInner = usePointerIsTransformerInner();
  185. const stage = useStage();
  186. return (shape: EntityShape) => {
  187. const inner = ref(true);
  188. const $stage = stage.value!.getNode();
  189. const updateInner = () => {
  190. inner.value =
  191. transformer.isTransforming() ||
  192. (transformer.queueShapes.value.includes(shape) &&
  193. pointerIsTransformerInner());
  194. };
  195. const stop = watch(
  196. transformer.queueShapes,
  197. (_a, _, onCleanup) => {
  198. updateInner();
  199. if (inner.value) {
  200. $stage.on("pointermove", updateInner);
  201. onCleanup(() => {
  202. $stage.off("pointermove", updateInner);
  203. });
  204. }
  205. },
  206. { immediate: true, flush: "sync" }
  207. );
  208. return [inner, stop] as const;
  209. };
  210. };
  211. export const useMouseShapesStatus = installGlobalVar(() => {
  212. const can = useCan();
  213. const stage = useStage();
  214. const listeners = ref([]) as Ref<EntityShape[]>;
  215. const hovers = ref([]) as Ref<EntityShape[]>;
  216. const press = ref([]) as Ref<EntityShape[]>;
  217. const selects = ref([]) as Ref<EntityShape[]>;
  218. const actives = ref([]) as Ref<EntityShape[]>;
  219. const operMode = useOperMode();
  220. const pointerIsTransformerInner = usePointerIsTransformerInner();
  221. const init = (stage: Stage) => {
  222. const prevent = computed(() => operMode.value.freeView);
  223. const [hover, hoverDestory] = getHoverShape(stage);
  224. const hoverChange = () => {
  225. if (prevent.value) {
  226. return;
  227. }
  228. hovers.value = hover.value
  229. ? shapeTreeContains(listeners.value, hover.value)
  230. : [];
  231. };
  232. const stopHoverCheck = watch(
  233. () => [hover.value, prevent.value],
  234. hoverChange
  235. );
  236. let downTime: number;
  237. let downPos: Pos | null = null;
  238. let downTarget: EntityShape | null;
  239. stage.on("pointerdown.mouse-status", (ev) => {
  240. if (ev.evt.button !== 0) return;
  241. downPos = { x: ev.evt.pageX, y: ev.evt.pageY };
  242. downTime = Date.now();
  243. if (prevent.value) return;
  244. const target = shapeTreeContain(listeners.value, ev.target);
  245. if (target && !press.value.includes(target)) {
  246. press.value.push(target);
  247. }
  248. downTarget = target;
  249. });
  250. return mergeFuns(
  251. stopHoverCheck,
  252. hoverDestory,
  253. listener(
  254. stage.container().parentElement as HTMLDivElement,
  255. "pointerup",
  256. async (ev) => {
  257. if (ev.button !== 0) return;
  258. const target = downTarget;
  259. const moveDis = downPos
  260. ? lineLen(downPos!, { x: ev.pageX, y: ev.pageY })
  261. : 3;
  262. downPos = null;
  263. downTarget = null;
  264. const isMove = moveDis > 2;
  265. if (prevent.value) return;
  266. press.value = [];
  267. if (Date.now() - downTime > 300 || isMove) return;
  268. if (pointerIsTransformerInner()) return;
  269. if (ev.button !== 0) {
  270. // actives.value = [];
  271. // if (!operMode.value.mulSelection) {
  272. // selects.value = [];
  273. // }
  274. return;
  275. }
  276. if (operMode.value.mulSelection) {
  277. if (!target) return;
  278. actives.value = [];
  279. const ndx = selects.value.findIndex(
  280. (item) => item.id() === target?.id()
  281. );
  282. if (~ndx) {
  283. selects.value.splice(ndx, 1);
  284. } else {
  285. selects.value.push(target!);
  286. }
  287. return;
  288. } else {
  289. selects.value = [];
  290. actives.value = target ? [target] : [];
  291. }
  292. }
  293. ),
  294. // listener(
  295. // stage.container().parentElement as HTMLDivElement,
  296. // "pointermove",
  297. // async () => {
  298. // if (prevent.value) return;
  299. // if (downTarget && !operMode.value.mulSelection) {
  300. // selects.value = [];
  301. // }
  302. // }
  303. // ),
  304. () => {
  305. listeners.value.forEach((shape) => {
  306. shape.off("pointerleave.mouse-status");
  307. });
  308. stage.off("pointerdown.mouse-status");
  309. hovers.value = [];
  310. actives.value = [];
  311. press.value = [];
  312. selects.value = [];
  313. }
  314. );
  315. };
  316. let cleanup: () => void;
  317. const stopStatusWatch = globalWatch(
  318. () => can.mouseReact,
  319. (current, prev) => {
  320. if (inRevise(prev, current)) {
  321. cleanup! && cleanup();
  322. if (current) {
  323. cleanup = init(stage.value!.getNode());
  324. }
  325. }
  326. },
  327. { immediate: true }
  328. );
  329. const pauseShapes = ref(new Set<EntityShape>());
  330. const getShapes = (shapes: Ref<EntityShape[]>) =>
  331. computed({
  332. get: () => {
  333. return shapes.value
  334. .filter((shape) => !pauseShapes.value.has(shape))
  335. .map(toRaw);
  336. },
  337. set: (val: EntityShape[]) => {
  338. shapes.value = val;
  339. },
  340. });
  341. const status = reactive({
  342. hovers: getShapes(hovers),
  343. actives: getShapes(actives),
  344. selects: getShapes(selects),
  345. press: getShapes(press),
  346. listeners,
  347. pause(shape: EntityShape) {
  348. pauseShapes.value.add(shape);
  349. },
  350. resume(shape: EntityShape) {
  351. pauseShapes.value.delete(shape);
  352. },
  353. });
  354. return {
  355. var: status,
  356. onDestroy: () => {
  357. stopStatusWatch();
  358. cleanup && cleanup();
  359. },
  360. };
  361. }, Symbol("mouseStatus"));
  362. export const useMouseShapeStatus = (
  363. shape: Ref<DC<EntityShape> | undefined>
  364. ) => {
  365. const status = useMouseShapesStatus();
  366. const shapeStatus = computed(() => {
  367. const $shape = shape.value?.getStage() as Shape;
  368. return {
  369. hover: status.hovers.includes($shape),
  370. active: status.actives.includes($shape),
  371. press: status.press.includes($shape),
  372. select: status.selects.includes($shape),
  373. pause: () =>
  374. shape.value?.getNode() && status.pause(shape.value?.getNode()),
  375. resume: () =>
  376. shape.value?.getNode() && status.resume(shape.value?.getNode()),
  377. };
  378. });
  379. watch(
  380. () => shape.value?.getStage(),
  381. (shape, _, onCleanup) => {
  382. if (shape) {
  383. if (status.listeners.includes(shape)) return;
  384. status.listeners.push(shape);
  385. onCleanup(() => {
  386. for (const key in status) {
  387. const k = key as keyof typeof status;
  388. if (Array.isArray(status[k])) {
  389. const ndx = status[k].indexOf(shape);
  390. ~ndx && status[k].splice(ndx, 1);
  391. }
  392. }
  393. });
  394. }
  395. },
  396. { immediate: true }
  397. );
  398. return shapeStatus;
  399. };
  400. export const useActiveItem = <T extends EntityShape>() => {
  401. const status = useMouseShapesStatus();
  402. const store = useStore();
  403. return computed(() => {
  404. if (!status.actives[0]) return;
  405. let shape = status.actives[0] as T;
  406. shape =
  407. ((shape as Group)?.findOne &&
  408. ((shape as Group)?.findOne(".repShape") as T)) ||
  409. shape;
  410. return {
  411. shape,
  412. item: store.getItemById(status.actives[0].id()),
  413. };
  414. });
  415. };
  416. type MouseStyleProps<T extends ShapeType> = {
  417. shape?: Ref<DC<EntityShape> | undefined>;
  418. getMouseStyle: (data: any) => Record<string, any>;
  419. data: Ref<DrawItem<T>>;
  420. };
  421. export const useMouseStyle = <T extends ShapeType>(
  422. props: MouseStyleProps<T>
  423. ) => {
  424. const shape = props.shape || ref();
  425. const status = useMouseShapeStatus(shape);
  426. const transformIngShapes = useTransformIngShapes();
  427. const mouseStyle = computed(() => {
  428. return props.getMouseStyle(props.data.value as any) as any;
  429. });
  430. const getStyle = () => {
  431. const styleMap = new Map([[mouseStyle.value.default, true]]);
  432. if ("hover" in mouseStyle.value) {
  433. styleMap.set(mouseStyle.value.hover, status.value.hover);
  434. }
  435. if ("press" in mouseStyle.value) {
  436. styleMap.set(mouseStyle.value.press, status.value.press);
  437. }
  438. if ("focus" in mouseStyle.value) {
  439. styleMap.set(mouseStyle.value.focus, status.value.active);
  440. }
  441. if (
  442. "drag" in mouseStyle.value &&
  443. transformIngShapes.value.includes(shape.value?.getNode()!)
  444. ) {
  445. styleMap.set(mouseStyle.value.drag, true);
  446. }
  447. if ("select" in mouseStyle.value) {
  448. styleMap.set(mouseStyle.value.select, status.value.select);
  449. }
  450. const finalStyle = {};
  451. for (const [style, use] of styleMap.entries()) {
  452. use && Object.assign(finalStyle as any, style);
  453. }
  454. return finalStyle;
  455. };
  456. const style = ref();
  457. watchEffect(() => {
  458. style.value = getStyle();
  459. });
  460. return { currentStyle: style, status, shape };
  461. };
  462. export const useAnimationMouseStyle = <T extends ShapeType>(
  463. props: MouseStyleProps<T>
  464. ) => {
  465. const { currentStyle, status } = useMouseStyle(props);
  466. // const [data, pauseAnimation, resumeAnimation] = useAniamtion(
  467. // currentStyle as any
  468. // );
  469. return [
  470. currentStyle,
  471. () => {
  472. // pauseAnimation();
  473. status.value.pause();
  474. },
  475. () => {
  476. status.value.resume();
  477. // resumeAnimation();
  478. },
  479. ] as const;
  480. };