use-transformer.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import { useMouseShapeStatus } from "./use-mouse-status.ts";
  2. import { Ref, ref, watch } from "vue";
  3. import { DC, EntityShape } from "../../deconstruction";
  4. import {
  5. installGlobalVar,
  6. useCan,
  7. useMode,
  8. useStage,
  9. useTransformIngShapes,
  10. } from "./use-global-vars.ts";
  11. import { Mode } from "../../constant/mode.ts";
  12. import { Transform, Util } from "konva/lib/Util";
  13. import { Pos, vector } from "@/utils/math.ts";
  14. import { useConversionPosition } from "./use-coversion-position.ts";
  15. import { getOffset, listener } from "@/utils/event.ts";
  16. import { flatPositions, mergeFuns, round } from "@/utils/shared.ts";
  17. import { Line } from "konva/lib/shapes/Line";
  18. import { setShapeTransform } from "@/utils/shape.ts";
  19. import { Transformer } from "../transformer.ts";
  20. import { TransformerConfig } from "konva/lib/shapes/Transformer";
  21. import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
  22. import { useComponentSnap } from "./use-snap.ts";
  23. import { useViewerInvertTransform, useViewerTransform } from "./use-viewer.ts";
  24. import { Rect } from "konva/lib/shapes/Rect";
  25. import { Text } from "konva/lib/shapes/Text";
  26. import { Group } from "konva/lib/Group";
  27. import { BaseItem } from "../components/util.ts";
  28. import { useGetComponentData } from "./use-component.ts";
  29. export type TransformerExtends = Transformer & {
  30. queueShapes: Ref<EntityShape[]>;
  31. };
  32. export const useTransformer = installGlobalVar(() => {
  33. const anchorCornerRadius = 5;
  34. const transformer = new Transformer({
  35. borderStrokeWidth: 2,
  36. borderStroke: themeMouseColors.pub,
  37. anchorCornerRadius,
  38. anchorSize: anchorCornerRadius * 2,
  39. rotationSnaps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360],
  40. rotationSnapTolerance: 3,
  41. anchorStrokeWidth: 2,
  42. anchorStroke: themeMouseColors.pub,
  43. anchorFill: themeMouseColors.press,
  44. flipEnabled: false,
  45. padding: 10,
  46. useSingleNodeRotation: true,
  47. }) as TransformerExtends;
  48. transformer.queueShapes = ref([]);
  49. transformer.on("transformstart.attachText", () => {
  50. const operateType = transformer.getActiveAnchor() as TransformerVectorType;
  51. if (operateType !== "rotater") return;
  52. const $node = transformer.findOne<Rect>(".rotater")!;
  53. const $g = new Group();
  54. const $text = new Text({
  55. listening: false,
  56. fill: themeColor,
  57. fontSize: 12,
  58. width: 100,
  59. align: "center",
  60. offset: { x: 50, y: 0 },
  61. });
  62. $g.add($text);
  63. $node.parent!.add($g);
  64. setShapeTransform($g, $node.getTransform());
  65. $g.y($g.y() - 2 * $text.fontSize());
  66. const updateText = () => {
  67. const rotation = transformer.rotation();
  68. $text.rotation(-rotation).text(` ${round(rotation, 1)}°`);
  69. };
  70. updateText();
  71. transformer.on("transform.attachText", updateText);
  72. transformer.on("transformend.attachText", () => {
  73. transformer.off("transform.attachText transformend.attachText");
  74. $g.destroy();
  75. });
  76. });
  77. return transformer;
  78. }, Symbol("transformer"));
  79. export const usePointerIsTransformerInner = () => {
  80. const transformer = useTransformer();
  81. const stage = useStage();
  82. return () => {
  83. const $stage = stage.value!.getStage();
  84. const tfRect = transformer.getClientRect();
  85. const padding = transformer.padding();
  86. tfRect.x -= padding;
  87. tfRect.y -= padding;
  88. tfRect.width += padding;
  89. tfRect.height += padding;
  90. const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 };
  91. return Util.haveIntersection(tfRect, pointRect);
  92. };
  93. };
  94. export type ScaleVectorType =
  95. | "middle-left"
  96. | "middle-right"
  97. | "top-center"
  98. | "bottom-center"
  99. | "top-right"
  100. | "top-left"
  101. | "bottom-right"
  102. | "bottom-left";
  103. export type TransformerVectorType = ScaleVectorType | "rotater";
  104. export const useGetTransformerOperType = () => {
  105. const transformer = useTransformer();
  106. return () => {
  107. if (!transformer.nodes().length) return null;
  108. return transformer.getActiveAnchor() as TransformerVectorType;
  109. };
  110. };
  111. export const useGetTransformerVectors = () => {
  112. const viewerInvertTransform = useViewerInvertTransform();
  113. const transformer = useTransformer();
  114. return (type: TransformerVectorType): Pos | null => {
  115. if (!transformer.nodes().length) return null;
  116. const merTransform = viewerInvertTransform.value
  117. .copy()
  118. .multiply(transformer.getTransform());
  119. const getVector = (operateType: TransformerVectorType): Pos => {
  120. if (operateType === "rotater") {
  121. return vector(getVector("bottom-left"))
  122. .add(getVector("bottom-right"))
  123. .add(getVector("top-left"))
  124. .add(getVector("top-right"))
  125. .divideScalar(4);
  126. } else {
  127. const centerNode = transformer.findOne(`.${operateType}`)!;
  128. return {
  129. x: centerNode.x(),
  130. y: centerNode.y(),
  131. };
  132. }
  133. };
  134. return merTransform.point(getVector(type));
  135. };
  136. };
  137. export const useGetTransformerOperDirection = () => {
  138. const getTransformerOperType = useGetTransformerOperType();
  139. const getTransformerVectors = useGetTransformerVectors();
  140. const originTypeMap = {
  141. "middle-left": "middle-right",
  142. "middle-right": "middle-left",
  143. "top-center": "bottom-center",
  144. "bottom-center": "top-center",
  145. "top-right": "bottom-left",
  146. "top-left": "bottom-right",
  147. "bottom-right": "top-left",
  148. "bottom-left": "top-right",
  149. rotater: "rotater",
  150. } as const;
  151. return () => {
  152. const operateType = getTransformerOperType();
  153. if (!operateType || !originTypeMap[operateType]) return null;
  154. const origin = getTransformerVectors(originTypeMap[operateType]);
  155. const operTarget = getTransformerVectors(operateType);
  156. return origin && operTarget && ([origin, operTarget] as const);
  157. };
  158. };
  159. export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
  160. const offset = ref<Pos>();
  161. const mode = useMode();
  162. const can = useCan();
  163. const conversion = useConversionPosition(true);
  164. const transformIngShapes = useTransformIngShapes();
  165. const status = useMouseShapeStatus(shape)
  166. const init = (shape: EntityShape) => {
  167. const dom = shape.getStage()!.container();
  168. let start: Pos | undefined;
  169. const enter = (position: Pos) => {
  170. mode.push(Mode.update);
  171. if (!start) {
  172. start = position;
  173. if (!can.dragMode) return;
  174. mode.add(Mode.draging);
  175. transformIngShapes.value.push(shape);
  176. }
  177. };
  178. const leave = () => {
  179. if (start) {
  180. offset.value = void 0;
  181. mode.pop();
  182. start = void 0;
  183. const ndx = transformIngShapes.value.indexOf(shape);
  184. ~ndx && transformIngShapes.value.splice(ndx, 1);
  185. }
  186. };
  187. shape.draggable(true);
  188. shape.dragBoundFunc((_, ev) => {
  189. if (!start) {
  190. enter(ev);
  191. } else if (can.dragMode) {
  192. const end = conversion(getOffset(ev, dom));
  193. offset.value = {
  194. x: end.x - start.x,
  195. y: end.y - start.y,
  196. };
  197. }
  198. return shape.absolutePosition();
  199. });
  200. shape.on("pointerdown.mouse-drag", (ev) => {
  201. enter(conversion(getOffset(ev.evt)));
  202. });
  203. return mergeFuns([
  204. () => {
  205. shape.draggable(false);
  206. shape.off("pointerdown.mouse-drag");
  207. start && leave();
  208. },
  209. listener(document.documentElement, "pointerup", () => {
  210. start && leave();
  211. }),
  212. ]);
  213. };
  214. watch(
  215. () =>
  216. (can.editMode || mode.include(Mode.update)) &&
  217. (status.value.active || status.value.hover),
  218. (canEdit, _, onCleanup) => {
  219. canEdit && onCleanup(init(shape.value!.getNode()));
  220. }
  221. );
  222. return offset;
  223. };
  224. type Rep<T> = {
  225. tempShape: T;
  226. init?: () => void;
  227. update?: () => void;
  228. destory: () => void;
  229. };
  230. const emptyFn = () => {};
  231. export const useShapeTransformer = <T extends EntityShape>(
  232. shape: Ref<DC<T> | undefined>,
  233. transformerConfig: TransformerConfig = {},
  234. replaceShape?: (shape: T) => Rep<T>,
  235. handlerTransform?: (transform: Transform) => Transform
  236. ) => {
  237. const offset = useShapeDrag(shape);
  238. const transform = ref<Transform>();
  239. const status = useMouseShapeStatus(shape);
  240. const mode = useMode();
  241. const transformer = useTransformer();
  242. const transformIngShapes = useTransformIngShapes();
  243. const viewTransform = useViewerTransform();
  244. const getComponentData = useGetComponentData();
  245. const can = useCan();
  246. const init = ($shape: T) => {
  247. let rep: Rep<T>;
  248. if (replaceShape) {
  249. rep = replaceShape($shape);
  250. } else {
  251. rep = {
  252. tempShape: $shape,
  253. destory: emptyFn,
  254. update: emptyFn,
  255. };
  256. }
  257. const updateTransform = () => {
  258. if (!can.dragMode) return;
  259. let appleTransform = rep.tempShape.getTransform().copy();
  260. if (handlerTransform) {
  261. appleTransform = handlerTransform(appleTransform);
  262. setShapeTransform(rep.tempShape, appleTransform);
  263. }
  264. transform.value = appleTransform;
  265. };
  266. rep.tempShape.on("transform.shapemer", updateTransform);
  267. const boundHandler = () => rep.update && rep.update();
  268. $shape.on("bound-change", boundHandler);
  269. // 拖拽时要更新矩阵
  270. let prevMoveTf: Transform | null = null;
  271. const stopDragWatch = watch(
  272. offset,
  273. (translate, oldTranslate) => {
  274. if (translate) {
  275. if (!oldTranslate) {
  276. rep.update && rep.update();
  277. }
  278. const moveTf = new Transform().translate(translate.x, translate.y);
  279. const finalTf = moveTf.copy();
  280. prevMoveTf && finalTf.multiply(prevMoveTf.invert());
  281. finalTf.multiply(rep.tempShape.getTransform());
  282. prevMoveTf = moveTf;
  283. setShapeTransform(rep.tempShape, finalTf);
  284. rep.tempShape.fire("transform");
  285. } else {
  286. prevMoveTf = null;
  287. transform.value = void 0;
  288. }
  289. },
  290. { immediate: true }
  291. );
  292. const stopTransformerWatch = watch(
  293. () => status.value.active,
  294. (active, _, onCleanup) => {
  295. const parent = $shape.parent;
  296. if (!(active && parent)) return;
  297. const oldConfig: TransformerConfig = {};
  298. for (const key in transformerConfig) {
  299. oldConfig[key] = (transformer as any)[key]();
  300. (transformer as any)[key](transformerConfig[key]);
  301. }
  302. transformer.nodes([rep.tempShape]);
  303. transformer.queueShapes.value = [$shape];
  304. parent.add(transformer);
  305. let isEnter = false;
  306. const downHandler = () => {
  307. if (isEnter) {
  308. mode.pop();
  309. }
  310. mode.push(Mode.update);
  311. isEnter = true;
  312. if (!can.dragMode) return;
  313. rep.update && rep.update();
  314. mode.add(Mode.draging);
  315. transformIngShapes.value.push($shape);
  316. };
  317. transformer.on("pointerdown.shapemer", downHandler);
  318. const stopPointupListener = listener(
  319. $shape.getStage()!.container(),
  320. "pointerup",
  321. () => {
  322. if (isEnter) {
  323. mode.pop();
  324. transform.value = void 0;
  325. isEnter = false;
  326. const ndx = transformIngShapes.value.indexOf($shape);
  327. ~ndx && transformIngShapes.value.splice(ndx, 1);
  328. }
  329. }
  330. );
  331. const stopTransformerForceUpdate = watch(
  332. viewTransform,
  333. () => transformer.forceUpdate(),
  334. { flush: "post" }
  335. );
  336. const stopLeaveUpdate = watch(
  337. () => getComponentData($shape).value,
  338. (val) => {
  339. rep.update && rep.update();
  340. },
  341. { flush: "post", deep: true }
  342. );
  343. onCleanup(() => {
  344. for (const key in oldConfig) {
  345. (transformer as any)[key](oldConfig[key]);
  346. }
  347. stopTransformerForceUpdate();
  348. stopPointupListener();
  349. stopLeaveUpdate();
  350. // TODO: 有可能transformer已经转移
  351. if (transformer.queueShapes.value.includes($shape)) {
  352. transformer.nodes([]);
  353. transformer.queueShapes.value = [];
  354. }
  355. transform.value = void 0;
  356. if (isEnter) {
  357. mode.pop();
  358. const ndx = transformIngShapes.value.indexOf($shape);
  359. ~ndx && transformIngShapes.value.splice(ndx, 1);
  360. }
  361. transformer.off("pointerdown.shapemer", downHandler);
  362. });
  363. }
  364. );
  365. return () => {
  366. $shape.off("bound-change", boundHandler);
  367. rep.tempShape.off("transform.shapemer", updateTransform);
  368. stopDragWatch();
  369. stopTransformerWatch();
  370. rep.destory();
  371. };
  372. };
  373. watch(
  374. () => shape.value,
  375. (shape, _) => {
  376. if (!shape) return;
  377. watch(
  378. () =>
  379. (can.editMode || mode.include(Mode.update)) &&
  380. (status.value.active || status.value.hover),
  381. (canEdit, _, onCleanup) => {
  382. if (canEdit) {
  383. const stop = init(shape.getStage());
  384. onCleanup(stop);
  385. } else {
  386. onCleanup(() => {});
  387. }
  388. },
  389. { immediate: true }
  390. );
  391. }
  392. );
  393. return transform;
  394. };
  395. export const cloneRepShape = <T extends EntityShape>(
  396. shape: T
  397. ): ReturnType<GetRepShape<T, any>> => {
  398. shape =
  399. ((shape as Group)?.findOne &&
  400. ((shape as Group)?.findOne(".repShape") as T)) ||
  401. shape;
  402. return {
  403. shape: shape.clone({
  404. fill: "rgb(0, 255, 0)",
  405. visible: false,
  406. strokeWidth: 0,
  407. }),
  408. update: (_, rep) => {
  409. setShapeTransform(rep, shape.getTransform());
  410. },
  411. };
  412. };
  413. export const transformerRepShapeHandler = <T extends EntityShape>(
  414. shape: T,
  415. repShape: T
  416. ) => {
  417. if (import.meta.env.DEV) {
  418. repShape.visible(true);
  419. repShape.opacity(0.1);
  420. }
  421. shape.parent!.add(repShape);
  422. repShape.zIndex(shape.getZIndex());
  423. repShape.listening(false);
  424. shape.repShape = repShape;
  425. return [
  426. repShape,
  427. () => {
  428. shape.repShape = undefined;
  429. repShape.remove();
  430. },
  431. ] as const;
  432. };
  433. type GetRepShape<T extends EntityShape, K extends object> = (shape: T) => {
  434. shape: T;
  435. update?: (data: K, shape: T) => void;
  436. };
  437. export type CustomTransformerProps<
  438. T extends BaseItem,
  439. S extends EntityShape
  440. > = {
  441. openSnap?: boolean;
  442. getRepShape?: GetRepShape<S, T>;
  443. beforeHandler?: (data: T, mat: Transform) => T;
  444. handler?: (data: T, mat: Transform) => Transform | void | true;
  445. callback?: (data: T, mat: Transform) => void;
  446. transformerConfig?: TransformerConfig;
  447. };
  448. export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
  449. shape: Ref<DC<S> | undefined>,
  450. data: Ref<T>,
  451. props: CustomTransformerProps<T, S>
  452. ) => {
  453. const { getRepShape, handler, callback, openSnap, transformerConfig } = props;
  454. const needSnap = openSnap && useComponentSnap(data.value.id);
  455. const transformer = useTransformer();
  456. let repResult: ReturnType<GetRepShape<S, T>>;
  457. const transform = useShapeTransformer(
  458. shape,
  459. transformerConfig,
  460. getRepShape &&
  461. ((shape) => {
  462. repResult = getRepShape(shape);
  463. const [_, destory] = transformerRepShapeHandler(shape, repResult.shape);
  464. return {
  465. tempShape: repResult.shape,
  466. update: () => {
  467. repResult.update && repResult.update(data.value, repResult.shape);
  468. },
  469. destory,
  470. };
  471. })
  472. );
  473. let callMat: Transform;
  474. watch(transform, (current, oldTransform) => {
  475. if (current) {
  476. if (!handler) return;
  477. const snapData = props.beforeHandler
  478. ? props.beforeHandler(data.value, current)
  479. : data.value;
  480. let nTransform;
  481. if (needSnap && (nTransform = needSnap[0](snapData))) {
  482. current = nTransform.multiply(current);
  483. }
  484. callMat = current;
  485. const mat = handler(data.value, current);
  486. if (mat) {
  487. if (repResult.update) {
  488. repResult.update(data.value, repResult.shape);
  489. } else if (mat !== true) {
  490. setShapeTransform(repResult.shape, mat);
  491. callMat = mat;
  492. }
  493. transformer.forceUpdate();
  494. }
  495. } else if (oldTransform) {
  496. needSnap && needSnap[1]();
  497. callback && callback(data.value, callMat);
  498. }
  499. });
  500. return transform;
  501. };
  502. export type LineTransformerData = BaseItem & {
  503. points: Pos[];
  504. attitude: number[];
  505. };
  506. export const useLineTransformer = <T extends LineTransformerData>(
  507. shape: Ref<DC<Line> | undefined>,
  508. data: Ref<T>,
  509. callback: (data: T) => void,
  510. genRepShape?: ($shape: Line) => Line
  511. ) => {
  512. let tempShape: Line;
  513. let inverAttitude: Transform;
  514. let stableVs = data.value.points;
  515. let tempVs = data.value.points;
  516. useCustomTransformer(shape, data, {
  517. openSnap: true,
  518. beforeHandler(data, mat) {
  519. const transfrom = mat.copy().multiply(inverAttitude);
  520. return {
  521. ...data,
  522. points: stableVs.map((v) => transfrom.point(v)),
  523. };
  524. },
  525. handler(data, mat) {
  526. // 顶点更新
  527. const transfrom = mat.copy().multiply(inverAttitude);
  528. data.points = tempVs = stableVs.map((v) => transfrom.point(v));
  529. },
  530. callback(data, mat) {
  531. data.attitude = mat.m;
  532. data.points = stableVs = tempVs;
  533. callback(data);
  534. },
  535. getRepShape($shape) {
  536. let repShape: Line;
  537. if (genRepShape) {
  538. repShape = genRepShape($shape);
  539. } else {
  540. repShape = cloneRepShape($shape).shape;
  541. }
  542. repShape = (repShape as any).points
  543. ? repShape
  544. : (repShape as unknown as Group).findOne<Line>(".line")!;
  545. tempShape = repShape;
  546. const update = (data: T) => {
  547. const attitude = new Transform(data.attitude);
  548. const inverMat = attitude.copy().invert();
  549. setShapeTransform(repShape, attitude);
  550. const initVs = data.points.map((v) => inverMat.point(v));
  551. stableVs = tempVs = data.points;
  552. repShape.points(flatPositions(initVs));
  553. repShape.closed(true);
  554. inverAttitude = inverMat;
  555. };
  556. update(data.value);
  557. return {
  558. update,
  559. shape: repShape,
  560. };
  561. },
  562. });
  563. };
  564. export const useMatCompTransformer = <T extends BaseItem & { mat: number[] }>(
  565. shape: Ref<DC<EntityShape> | undefined>,
  566. data: Ref<T>,
  567. callback: (data: T) => void
  568. ) => {
  569. return useCustomTransformer(shape, data, {
  570. beforeHandler(data, mat) {
  571. return { ...data, mat: mat.m };
  572. },
  573. handler(data, mat) {
  574. data.mat = mat.m;
  575. },
  576. getRepShape: cloneRepShape,
  577. callback,
  578. openSnap: true,
  579. });
  580. };