actionFinalize.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { KEYS } from "../keys";
  2. import { isInvisiblySmallElement } from "../element";
  3. import { resetCursor } from "../utils";
  4. import { ToolButton } from "../components/ToolButton";
  5. import { done } from "../components/icons";
  6. import { t } from "../i18n";
  7. import { register } from "./register";
  8. import { mutateElement } from "../element/mutateElement";
  9. import { isPathALoop } from "../math";
  10. import { LinearElementEditor } from "../element/linearElementEditor";
  11. import Scene from "../scene/Scene";
  12. import {
  13. maybeBindLinearElement,
  14. bindOrUnbindLinearElement,
  15. } from "../element/binding";
  16. import { isBindingElement } from "../element/typeChecks";
  17. export const actionFinalize = register({
  18. name: "finalize",
  19. perform: (elements, appState, _, { canvas, focusContainer }) => {
  20. if (appState.editingLinearElement) {
  21. const { elementId, startBindingElement, endBindingElement } =
  22. appState.editingLinearElement;
  23. const element = LinearElementEditor.getElement(elementId);
  24. if (element) {
  25. if (isBindingElement(element)) {
  26. bindOrUnbindLinearElement(
  27. element,
  28. startBindingElement,
  29. endBindingElement,
  30. );
  31. }
  32. return {
  33. elements:
  34. element.points.length < 2 || isInvisiblySmallElement(element)
  35. ? elements.filter((el) => el.id !== element.id)
  36. : undefined,
  37. appState: {
  38. ...appState,
  39. editingLinearElement: null,
  40. },
  41. commitToHistory: true,
  42. };
  43. }
  44. }
  45. let newElements = elements;
  46. if (appState.pendingImageElement) {
  47. mutateElement(appState.pendingImageElement, { isDeleted: true }, false);
  48. }
  49. if (window.document.activeElement instanceof HTMLElement) {
  50. focusContainer();
  51. }
  52. const multiPointElement = appState.multiElement
  53. ? appState.multiElement
  54. : appState.editingElement?.type === "freedraw"
  55. ? appState.editingElement
  56. : null;
  57. if (multiPointElement) {
  58. // pen and mouse have hover
  59. if (
  60. multiPointElement.type !== "freedraw" &&
  61. appState.lastPointerDownWith !== "touch"
  62. ) {
  63. const { points, lastCommittedPoint } = multiPointElement;
  64. if (
  65. !lastCommittedPoint ||
  66. points[points.length - 1] !== lastCommittedPoint
  67. ) {
  68. mutateElement(multiPointElement, {
  69. points: multiPointElement.points.slice(0, -1),
  70. });
  71. }
  72. }
  73. if (isInvisiblySmallElement(multiPointElement)) {
  74. newElements = newElements.slice(0, -1);
  75. }
  76. // If the multi point line closes the loop,
  77. // set the last point to first point.
  78. // This ensures that loop remains closed at different scales.
  79. const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
  80. if (
  81. multiPointElement.type === "line" ||
  82. multiPointElement.type === "freedraw"
  83. ) {
  84. if (isLoop) {
  85. const linePoints = multiPointElement.points;
  86. const firstPoint = linePoints[0];
  87. mutateElement(multiPointElement, {
  88. points: linePoints.map((point, index) =>
  89. index === linePoints.length - 1
  90. ? ([firstPoint[0], firstPoint[1]] as const)
  91. : point,
  92. ),
  93. });
  94. }
  95. }
  96. if (
  97. isBindingElement(multiPointElement) &&
  98. !isLoop &&
  99. multiPointElement.points.length > 1
  100. ) {
  101. const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
  102. multiPointElement,
  103. -1,
  104. );
  105. maybeBindLinearElement(
  106. multiPointElement,
  107. appState,
  108. Scene.getScene(multiPointElement)!,
  109. { x, y },
  110. );
  111. }
  112. if (!appState.elementLocked && appState.elementType !== "freedraw") {
  113. appState.selectedElementIds[multiPointElement.id] = true;
  114. }
  115. }
  116. if (
  117. (!appState.elementLocked && appState.elementType !== "freedraw") ||
  118. !multiPointElement
  119. ) {
  120. resetCursor(canvas);
  121. }
  122. return {
  123. elements: newElements,
  124. appState: {
  125. ...appState,
  126. elementType:
  127. (appState.elementLocked || appState.elementType === "freedraw") &&
  128. multiPointElement
  129. ? appState.elementType
  130. : "selection",
  131. draggingElement: null,
  132. multiElement: null,
  133. editingElement: null,
  134. startBoundElement: null,
  135. suggestedBindings: [],
  136. selectedElementIds:
  137. multiPointElement &&
  138. !appState.elementLocked &&
  139. appState.elementType !== "freedraw"
  140. ? {
  141. ...appState.selectedElementIds,
  142. [multiPointElement.id]: true,
  143. }
  144. : appState.selectedElementIds,
  145. pendingImageElement: null,
  146. },
  147. commitToHistory: appState.elementType === "freedraw",
  148. };
  149. },
  150. keyTest: (event, appState) =>
  151. (event.key === KEYS.ESCAPE &&
  152. (appState.editingLinearElement !== null ||
  153. (!appState.draggingElement && appState.multiElement === null))) ||
  154. ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
  155. appState.multiElement !== null),
  156. PanelComponent: ({ appState, updateData }) => (
  157. <ToolButton
  158. type="button"
  159. icon={done}
  160. title={t("buttons.done")}
  161. aria-label={t("buttons.done")}
  162. onClick={updateData}
  163. visible={appState.multiElement != null}
  164. />
  165. ),
  166. });