@excalidraw/excalidraw 0.17.1-3e334a6 → 0.17.1-4689a6b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/dist/browser/dev/excalidraw-assets-dev/{chunk-M7HSOQ7X.js → chunk-23CKV3WP.js} +3 -1
  3. package/dist/browser/dev/excalidraw-assets-dev/chunk-23CKV3WP.js.map +7 -0
  4. package/dist/browser/dev/excalidraw-assets-dev/{chunk-RWZVJAQU.js → chunk-7D5BMEAB.js} +2227 -1976
  5. package/dist/browser/dev/excalidraw-assets-dev/chunk-7D5BMEAB.js.map +7 -0
  6. package/dist/browser/dev/excalidraw-assets-dev/{en-R45KN4KN.js → en-W7TECCRB.js} +2 -2
  7. package/dist/browser/dev/excalidraw-assets-dev/{image-EDKQZH7Z.js → image-JKT6GXZD.js} +2 -2
  8. package/dist/browser/dev/index.css +20 -0
  9. package/dist/browser/dev/index.css.map +2 -2
  10. package/dist/browser/dev/index.js +766 -580
  11. package/dist/browser/dev/index.js.map +4 -4
  12. package/dist/browser/prod/excalidraw-assets/chunk-DWOM5R6H.js +55 -0
  13. package/dist/browser/prod/excalidraw-assets/{chunk-DIHRGRYX.js → chunk-SK23VHAR.js} +1 -1
  14. package/dist/browser/prod/excalidraw-assets/{en-H6IY7PV6.js → en-SMMH575S.js} +1 -1
  15. package/dist/browser/prod/excalidraw-assets/image-WDEQS5RL.js +1 -0
  16. package/dist/browser/prod/index.css +1 -1
  17. package/dist/browser/prod/index.js +22 -22
  18. package/dist/{prod/en-N2RZZLK5.json → dev/en-CVBEBUBY.json} +2 -0
  19. package/dist/dev/index.css +20 -0
  20. package/dist/dev/index.css.map +2 -2
  21. package/dist/dev/index.js +2379 -2069
  22. package/dist/dev/index.js.map +4 -4
  23. package/dist/excalidraw/actions/actionBoundText.js +4 -1
  24. package/dist/excalidraw/actions/actionCanvas.js +3 -1
  25. package/dist/excalidraw/actions/actionDuplicateSelection.js +4 -0
  26. package/dist/excalidraw/actions/actionExport.d.ts +1 -1
  27. package/dist/excalidraw/actions/actionFinalize.d.ts +1 -1
  28. package/dist/excalidraw/actions/actionFinalize.js +3 -3
  29. package/dist/excalidraw/actions/actionFlip.d.ts +3 -3
  30. package/dist/excalidraw/actions/actionFlip.js +6 -6
  31. package/dist/excalidraw/actions/actionGroup.js +4 -2
  32. package/dist/excalidraw/actions/actionHistory.js +3 -0
  33. package/dist/excalidraw/actions/actionZindex.d.ts +11 -11
  34. package/dist/excalidraw/analytics.js +1 -1
  35. package/dist/excalidraw/components/App.d.ts +13 -3
  36. package/dist/excalidraw/components/App.js +211 -81
  37. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +24 -10
  38. package/dist/excalidraw/components/DarkModeToggle.js +3 -1
  39. package/dist/excalidraw/components/HelpDialog.js +2 -2
  40. package/dist/excalidraw/components/RadioGroup.d.ts +2 -1
  41. package/dist/excalidraw/components/RadioGroup.js +1 -1
  42. package/dist/excalidraw/components/TTDDialog/MermaidToExcalidraw.js +6 -2
  43. package/dist/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.d.ts +18 -0
  44. package/dist/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.js +9 -0
  45. package/dist/excalidraw/components/hyperlink/Hyperlink.js +3 -3
  46. package/dist/excalidraw/components/hyperlink/helpers.js +2 -3
  47. package/dist/excalidraw/components/icons.d.ts +3 -0
  48. package/dist/excalidraw/components/icons.js +5 -1
  49. package/dist/excalidraw/components/main-menu/DefaultItems.d.ts +12 -2
  50. package/dist/excalidraw/components/main-menu/DefaultItems.js +38 -7
  51. package/dist/excalidraw/constants.d.ts +0 -3
  52. package/dist/excalidraw/constants.js +0 -3
  53. package/dist/excalidraw/data/magic.js +2 -1
  54. package/dist/excalidraw/data/reconcile.d.ts +6 -0
  55. package/dist/excalidraw/data/reconcile.js +49 -0
  56. package/dist/excalidraw/data/restore.d.ts +3 -3
  57. package/dist/excalidraw/data/restore.js +5 -6
  58. package/dist/excalidraw/data/transform.d.ts +1 -1
  59. package/dist/excalidraw/data/transform.js +12 -3
  60. package/dist/excalidraw/element/binding.d.ts +22 -9
  61. package/dist/excalidraw/element/binding.js +403 -26
  62. package/dist/excalidraw/element/bounds.d.ts +0 -1
  63. package/dist/excalidraw/element/bounds.js +0 -3
  64. package/dist/excalidraw/element/collision.d.ts +14 -19
  65. package/dist/excalidraw/element/collision.js +36 -713
  66. package/dist/excalidraw/element/embeddable.js +18 -43
  67. package/dist/excalidraw/element/index.d.ts +0 -1
  68. package/dist/excalidraw/element/index.js +0 -1
  69. package/dist/excalidraw/element/linearElementEditor.d.ts +10 -10
  70. package/dist/excalidraw/element/linearElementEditor.js +6 -4
  71. package/dist/excalidraw/element/newElement.d.ts +1 -1
  72. package/dist/excalidraw/element/newElement.js +2 -1
  73. package/dist/excalidraw/element/textElement.d.ts +0 -1
  74. package/dist/excalidraw/element/textElement.js +0 -30
  75. package/dist/excalidraw/element/types.d.ts +17 -2
  76. package/dist/excalidraw/errors.d.ts +3 -0
  77. package/dist/excalidraw/errors.js +3 -0
  78. package/dist/excalidraw/fractionalIndex.d.ts +40 -0
  79. package/dist/excalidraw/fractionalIndex.js +241 -0
  80. package/dist/excalidraw/frame.d.ts +1 -1
  81. package/dist/excalidraw/hooks/useCreatePortalContainer.js +2 -1
  82. package/dist/excalidraw/locales/en.json +2 -0
  83. package/dist/excalidraw/renderer/helpers.js +2 -2
  84. package/dist/excalidraw/renderer/interactiveScene.js +1 -1
  85. package/dist/excalidraw/renderer/renderElement.js +3 -3
  86. package/dist/excalidraw/renderer/renderSnaps.js +2 -1
  87. package/dist/excalidraw/scene/Scene.d.ts +7 -6
  88. package/dist/excalidraw/scene/Scene.js +28 -13
  89. package/dist/excalidraw/scene/export.js +4 -3
  90. package/dist/excalidraw/types.d.ts +4 -3
  91. package/dist/excalidraw/utils.d.ts +1 -0
  92. package/dist/excalidraw/utils.js +1 -0
  93. package/dist/excalidraw/zindex.d.ts +2 -2
  94. package/dist/excalidraw/zindex.js +9 -13
  95. package/dist/{dev/en-N2RZZLK5.json → prod/en-CVBEBUBY.json} +2 -0
  96. package/dist/prod/index.css +1 -1
  97. package/dist/prod/index.js +36 -36
  98. package/dist/utils/collision.d.ts +4 -0
  99. package/dist/utils/collision.js +48 -0
  100. package/dist/utils/geometry/geometry.d.ts +71 -0
  101. package/dist/utils/geometry/geometry.js +674 -0
  102. package/dist/utils/geometry/shape.d.ts +55 -0
  103. package/dist/utils/geometry/shape.js +149 -0
  104. package/package.json +2 -1
  105. package/dist/browser/dev/excalidraw-assets-dev/chunk-M7HSOQ7X.js.map +0 -7
  106. package/dist/browser/dev/excalidraw-assets-dev/chunk-RWZVJAQU.js.map +0 -7
  107. package/dist/browser/prod/excalidraw-assets/chunk-LL4GORAM.js +0 -55
  108. package/dist/browser/prod/excalidraw-assets/image-EFCJDJH3.js +0 -1
  109. /package/dist/browser/dev/excalidraw-assets-dev/{en-R45KN4KN.js.map → en-W7TECCRB.js.map} +0 -0
  110. /package/dist/browser/dev/excalidraw-assets-dev/{image-EDKQZH7Z.js.map → image-JKT6GXZD.js.map} +0 -0
@@ -1,6 +1,6 @@
1
- import { ExcalidrawLinearElement, ExcalidrawBindableElement, NonDeleted, NonDeletedExcalidrawElement, ExcalidrawElement, ElementsMap, NonDeletedSceneElementsMap } from "./types";
2
- import { AppState } from "../types";
3
- import Scene from "../scene/Scene";
1
+ import * as GA from "../ga";
2
+ import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawRectangleElement, ExcalidrawDiamondElement, ExcalidrawTextElement, ExcalidrawEllipseElement, ExcalidrawImageElement, ExcalidrawFrameLikeElement, ExcalidrawIframeLikeElement, NonDeleted, ExcalidrawLinearElement, NonDeletedExcalidrawElement, ElementsMap, NonDeletedSceneElementsMap } from "./types";
3
+ import { AppClassProperties, AppState, Point } from "../types";
4
4
  export type SuggestedBinding = NonDeleted<ExcalidrawBindableElement> | SuggestedPointBinding;
5
5
  export type SuggestedPointBinding = [
6
6
  NonDeleted<ExcalidrawLinearElement>,
@@ -10,18 +10,18 @@ export type SuggestedPointBinding = [
10
10
  export declare const shouldEnableBindingForPointerEvent: (event: React.PointerEvent<HTMLElement>) => boolean;
11
11
  export declare const isBindingEnabled: (appState: AppState) => boolean;
12
12
  export declare const bindOrUnbindLinearElement: (linearElement: NonDeleted<ExcalidrawLinearElement>, startBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep", elementsMap: NonDeletedSceneElementsMap) => void;
13
- export declare const bindOrUnbindSelectedElements: (selectedElements: NonDeleted<ExcalidrawElement>[], elements: readonly ExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap) => void;
14
- export declare const maybeBindLinearElement: (linearElement: NonDeleted<ExcalidrawLinearElement>, appState: AppState, scene: Scene, pointerCoords: {
13
+ export declare const bindOrUnbindSelectedElements: (selectedElements: NonDeleted<ExcalidrawElement>[], app: AppClassProperties) => void;
14
+ export declare const maybeBindLinearElement: (linearElement: NonDeleted<ExcalidrawLinearElement>, appState: AppState, pointerCoords: {
15
15
  x: number;
16
16
  y: number;
17
- }, elementsMap: NonDeletedSceneElementsMap) => void;
17
+ }, app: AppClassProperties) => void;
18
18
  export declare const bindLinearElement: (linearElement: NonDeleted<ExcalidrawLinearElement>, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", elementsMap: NonDeletedSceneElementsMap) => void;
19
19
  export declare const isLinearElementSimpleAndAlreadyBound: (linearElement: NonDeleted<ExcalidrawLinearElement>, alreadyBoundToId: ExcalidrawBindableElement["id"] | undefined, bindableElement: ExcalidrawBindableElement) => boolean;
20
- export declare const unbindLinearElements: (elements: NonDeleted<ExcalidrawElement>[], elementsMap: NonDeletedSceneElementsMap) => void;
20
+ export declare const unbindLinearElements: (elements: readonly NonDeleted<ExcalidrawElement>[], elementsMap: NonDeletedSceneElementsMap) => void;
21
21
  export declare const getHoveredElementForBinding: (pointerCoords: {
22
22
  x: number;
23
23
  y: number;
24
- }, elements: readonly NonDeletedExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap) => NonDeleted<ExcalidrawBindableElement> | null;
24
+ }, app: AppClassProperties) => NonDeleted<ExcalidrawBindableElement> | null;
25
25
  export declare const updateBoundElements: (changedElement: NonDeletedExcalidrawElement, elementsMap: ElementsMap, options?: {
26
26
  simultaneouslyUpdated?: readonly ExcalidrawElement[];
27
27
  newSize?: {
@@ -29,6 +29,19 @@ export declare const updateBoundElements: (changedElement: NonDeletedExcalidrawE
29
29
  height: number;
30
30
  };
31
31
  }) => void;
32
- export declare const getEligibleElementsForBinding: (selectedElements: NonDeleted<ExcalidrawElement>[], elements: readonly ExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap) => SuggestedBinding[];
32
+ export declare const getEligibleElementsForBinding: (selectedElements: NonDeleted<ExcalidrawElement>[], app: AppClassProperties) => SuggestedBinding[];
33
33
  export declare const fixBindingsAfterDuplication: (sceneElements: readonly ExcalidrawElement[], oldElements: readonly ExcalidrawElement[], oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, duplicatesServeAsOld?: "duplicatesServeAsOld" | undefined) => void;
34
34
  export declare const fixBindingsAfterDeletion: (sceneElements: readonly ExcalidrawElement[], deletedElements: readonly ExcalidrawElement[]) => void;
35
+ export declare const bindingBorderTest: (element: NonDeleted<ExcalidrawBindableElement>, { x, y }: {
36
+ x: number;
37
+ y: number;
38
+ }, app: AppClassProperties) => boolean;
39
+ export declare const maxBindingGap: (element: ExcalidrawElement, elementWidth: number, elementHeight: number) => number;
40
+ export declare const distanceToBindableElement: (element: ExcalidrawBindableElement, point: readonly [number, number], elementsMap: ElementsMap) => number;
41
+ export declare const distanceToEllipse: (element: ExcalidrawEllipseElement, point: readonly [number, number], elementsMap: ElementsMap) => number;
42
+ export declare const determineFocusDistance: (element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], elementsMap: ElementsMap) => number;
43
+ export declare const determineFocusPoint: (element: ExcalidrawBindableElement, focus: number, adjecentPoint: readonly [number, number], elementsMap: ElementsMap) => readonly [number, number];
44
+ export declare const intersectElementWithLine: (element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap: number | undefined, elementsMap: ElementsMap) => Point[];
45
+ export declare const getCircleIntersections: (center: readonly [number, number, number, number, number, number, number, number], radius: number, line: readonly [number, number, number, number, number, number, number, number]) => GA.Point[];
46
+ export declare const findFocusPointForEllipse: (ellipse: ExcalidrawEllipseElement, relativeDistance: number, point: readonly [number, number, number, number, number, number, number, number]) => readonly [number, number, number, number, number, number, number, number];
47
+ export declare const findFocusPointForRectangulars: (element: ExcalidrawRectangleElement | ExcalidrawImageElement | ExcalidrawDiamondElement | ExcalidrawTextElement | ExcalidrawIframeLikeElement | ExcalidrawFrameLikeElement, relativeDistance: number, point: readonly [number, number, number, number, number, number, number, number]) => readonly [number, number, number, number, number, number, number, number];
@@ -1,6 +1,12 @@
1
+ import * as GA from "../ga";
2
+ import * as GAPoint from "../gapoints";
3
+ import * as GADirection from "../gadirections";
4
+ import * as GALine from "../galines";
5
+ import * as GATransform from "../gatransforms";
6
+ import { getElementAbsoluteCoords } from "./bounds";
7
+ import { isPointOnShape } from "../../utils/collision";
1
8
  import { getElementAtPosition } from "../scene";
2
9
  import { isBindableElement, isBindingElement, isLinearElement, } from "./typeChecks";
3
- import { bindingBorderTest, distanceToBindableElement, maxBindingGap, determineFocusDistance, intersectElementWithLine, determineFocusPoint, } from "./collision";
4
10
  import { mutateElement } from "./mutateElement";
5
11
  import Scene from "../scene/Scene";
6
12
  import { LinearElementEditor } from "./linearElementEditor";
@@ -61,27 +67,27 @@ unboundFromElementIds, elementsMap) => {
61
67
  }
62
68
  }
63
69
  };
64
- export const bindOrUnbindSelectedElements = (selectedElements, elements, elementsMap) => {
70
+ export const bindOrUnbindSelectedElements = (selectedElements, app) => {
65
71
  selectedElements.forEach((selectedElement) => {
66
72
  if (isBindingElement(selectedElement)) {
67
- bindOrUnbindLinearElement(selectedElement, getElligibleElementForBindingElement(selectedElement, "start", elements, elementsMap), getElligibleElementForBindingElement(selectedElement, "end", elements, elementsMap), elementsMap);
73
+ bindOrUnbindLinearElement(selectedElement, getElligibleElementForBindingElement(selectedElement, "start", app), getElligibleElementForBindingElement(selectedElement, "end", app), app.scene.getNonDeletedElementsMap());
68
74
  }
69
75
  else if (isBindableElement(selectedElement)) {
70
- maybeBindBindableElement(selectedElement, elementsMap);
76
+ maybeBindBindableElement(selectedElement, app.scene.getNonDeletedElementsMap(), app);
71
77
  }
72
78
  });
73
79
  };
74
- const maybeBindBindableElement = (bindableElement, elementsMap) => {
75
- getElligibleElementsForBindableElementAndWhere(bindableElement, elementsMap).forEach(([linearElement, where]) => bindOrUnbindLinearElement(linearElement, where === "end" ? "keep" : bindableElement, where === "start" ? "keep" : bindableElement, elementsMap));
80
+ const maybeBindBindableElement = (bindableElement, elementsMap, app) => {
81
+ getElligibleElementsForBindableElementAndWhere(bindableElement, app).forEach(([linearElement, where]) => bindOrUnbindLinearElement(linearElement, where === "end" ? "keep" : bindableElement, where === "start" ? "keep" : bindableElement, elementsMap));
76
82
  };
77
- export const maybeBindLinearElement = (linearElement, appState, scene, pointerCoords, elementsMap) => {
83
+ export const maybeBindLinearElement = (linearElement, appState, pointerCoords, app) => {
78
84
  if (appState.startBoundElement != null) {
79
- bindLinearElement(linearElement, appState.startBoundElement, "start", elementsMap);
85
+ bindLinearElement(linearElement, appState.startBoundElement, "start", app.scene.getNonDeletedElementsMap());
80
86
  }
81
- const hoveredElement = getHoveredElementForBinding(pointerCoords, scene.getNonDeletedElements(), elementsMap);
87
+ const hoveredElement = getHoveredElementForBinding(pointerCoords, app);
82
88
  if (hoveredElement != null &&
83
89
  !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(linearElement, hoveredElement, "end")) {
84
- bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
90
+ bindLinearElement(linearElement, hoveredElement, "end", app.scene.getNonDeletedElementsMap());
85
91
  }
86
92
  };
87
93
  export const bindLinearElement = (linearElement, hoveredElement, startOrEnd, elementsMap) => {
@@ -125,9 +131,9 @@ const unbindLinearElement = (linearElement, startOrEnd) => {
125
131
  mutateElement(linearElement, { [field]: null });
126
132
  return binding.elementId;
127
133
  };
128
- export const getHoveredElementForBinding = (pointerCoords, elements, elementsMap) => {
129
- const hoveredElement = getElementAtPosition(elements, (element) => isBindableElement(element, false) &&
130
- bindingBorderTest(element, pointerCoords, elementsMap));
134
+ export const getHoveredElementForBinding = (pointerCoords, app) => {
135
+ const hoveredElement = getElementAtPosition(app.scene.getNonDeletedElements(), (element) => isBindableElement(element, false) &&
136
+ bindingBorderTest(element, pointerCoords, app));
131
137
  return hoveredElement;
132
138
  };
133
139
  const calculateFocusAndGap = (linearElement, hoveredElement, startOrEnd, elementsMap) => {
@@ -237,28 +243,28 @@ const maybeCalculateNewGapWhenScaling = (changedElement, currentBinding, newSize
237
243
  return { elementId, gap: newGap, focus };
238
244
  };
239
245
  // TODO: this is a bottleneck, optimise
240
- export const getEligibleElementsForBinding = (selectedElements, elements, elementsMap) => {
246
+ export const getEligibleElementsForBinding = (selectedElements, app) => {
241
247
  const includedElementIds = new Set(selectedElements.map(({ id }) => id));
242
248
  return selectedElements.flatMap((selectedElement) => isBindingElement(selectedElement, false)
243
- ? getElligibleElementsForBindingElement(selectedElement, elements, elementsMap).filter((element) => !includedElementIds.has(element.id))
249
+ ? getElligibleElementsForBindingElement(selectedElement, app).filter((element) => !includedElementIds.has(element.id))
244
250
  : isBindableElement(selectedElement, false)
245
- ? getElligibleElementsForBindableElementAndWhere(selectedElement, elementsMap).filter((binding) => !includedElementIds.has(binding[0].id))
251
+ ? getElligibleElementsForBindableElementAndWhere(selectedElement, app).filter((binding) => !includedElementIds.has(binding[0].id))
246
252
  : []);
247
253
  };
248
- const getElligibleElementsForBindingElement = (linearElement, elements, elementsMap) => {
254
+ const getElligibleElementsForBindingElement = (linearElement, app) => {
249
255
  return [
250
- getElligibleElementForBindingElement(linearElement, "start", elements, elementsMap),
251
- getElligibleElementForBindingElement(linearElement, "end", elements, elementsMap),
256
+ getElligibleElementForBindingElement(linearElement, "start", app),
257
+ getElligibleElementForBindingElement(linearElement, "end", app),
252
258
  ].filter((element) => element != null);
253
259
  };
254
- const getElligibleElementForBindingElement = (linearElement, startOrEnd, elements, elementsMap) => {
255
- return getHoveredElementForBinding(getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), elements, elementsMap);
260
+ const getElligibleElementForBindingElement = (linearElement, startOrEnd, app) => {
261
+ return getHoveredElementForBinding(getLinearElementEdgeCoors(linearElement, startOrEnd, app.scene.getNonDeletedElementsMap()), app);
256
262
  };
257
263
  const getLinearElementEdgeCoors = (linearElement, startOrEnd, elementsMap) => {
258
264
  const index = startOrEnd === "start" ? 0 : -1;
259
265
  return tupleToCoors(LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index, elementsMap));
260
266
  };
261
- const getElligibleElementsForBindableElementAndWhere = (bindableElement, elementsMap) => {
267
+ const getElligibleElementsForBindableElementAndWhere = (bindableElement, app) => {
262
268
  const scene = Scene.getScene(bindableElement);
263
269
  return scene
264
270
  .getNonDeletedElements()
@@ -266,8 +272,8 @@ const getElligibleElementsForBindableElementAndWhere = (bindableElement, element
266
272
  if (!isBindingElement(element, false)) {
267
273
  return null;
268
274
  }
269
- const canBindStart = isLinearElementEligibleForNewBindingByBindable(element, "start", bindableElement, elementsMap);
270
- const canBindEnd = isLinearElementEligibleForNewBindingByBindable(element, "end", bindableElement, elementsMap);
275
+ const canBindStart = isLinearElementEligibleForNewBindingByBindable(element, "start", bindableElement, scene.getNonDeletedElementsMap(), app);
276
+ const canBindEnd = isLinearElementEligibleForNewBindingByBindable(element, "end", bindableElement, scene.getNonDeletedElementsMap(), app);
271
277
  if (!canBindStart && !canBindEnd) {
272
278
  return null;
273
279
  }
@@ -279,11 +285,11 @@ const getElligibleElementsForBindableElementAndWhere = (bindableElement, element
279
285
  })
280
286
  .filter((maybeElement) => maybeElement != null);
281
287
  };
282
- const isLinearElementEligibleForNewBindingByBindable = (linearElement, startOrEnd, bindableElement, elementsMap) => {
288
+ const isLinearElementEligibleForNewBindingByBindable = (linearElement, startOrEnd, bindableElement, elementsMap, app) => {
283
289
  const existingBinding = linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
284
290
  return (existingBinding == null &&
285
291
  !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(linearElement, bindableElement, startOrEnd) &&
286
- bindingBorderTest(bindableElement, getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), elementsMap));
292
+ bindingBorderTest(bindableElement, getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), app));
287
293
  };
288
294
  // We need to:
289
295
  // 1: Update elements not selected to point to duplicated elements
@@ -412,3 +418,374 @@ const newBoundElementsAfterDeletion = (boundElements, deletedElementIds) => {
412
418
  }
413
419
  return boundElements.filter((ele) => !deletedElementIds.has(ele.id));
414
420
  };
421
+ export const bindingBorderTest = (element, { x, y }, app) => {
422
+ const threshold = maxBindingGap(element, element.width, element.height);
423
+ const shape = app.getElementShape(element);
424
+ return isPointOnShape([x, y], shape, threshold);
425
+ };
426
+ export const maxBindingGap = (element, elementWidth, elementHeight) => {
427
+ // Aligns diamonds with rectangles
428
+ const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
429
+ const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
430
+ // We make the bindable boundary bigger for bigger elements
431
+ return Math.max(16, Math.min(0.25 * smallerDimension, 32));
432
+ };
433
+ export const distanceToBindableElement = (element, point, elementsMap) => {
434
+ switch (element.type) {
435
+ case "rectangle":
436
+ case "image":
437
+ case "text":
438
+ case "iframe":
439
+ case "embeddable":
440
+ case "frame":
441
+ case "magicframe":
442
+ return distanceToRectangle(element, point, elementsMap);
443
+ case "diamond":
444
+ return distanceToDiamond(element, point, elementsMap);
445
+ case "ellipse":
446
+ return distanceToEllipse(element, point, elementsMap);
447
+ }
448
+ };
449
+ const distanceToRectangle = (element, point, elementsMap) => {
450
+ const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point, elementsMap);
451
+ return Math.max(GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)), GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)));
452
+ };
453
+ const distanceToDiamond = (element, point, elementsMap) => {
454
+ const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point, elementsMap);
455
+ const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
456
+ return GAPoint.distanceToLine(pointRel, side);
457
+ };
458
+ export const distanceToEllipse = (element, point, elementsMap) => {
459
+ const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
460
+ return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
461
+ };
462
+ const ellipseParamsForTest = (element, point, elementsMap) => {
463
+ const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point, elementsMap);
464
+ const [px, py] = GAPoint.toTuple(pointRel);
465
+ // We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
466
+ let tx = 0.707;
467
+ let ty = 0.707;
468
+ const a = hwidth;
469
+ const b = hheight;
470
+ // This is a numerical method to find the params tx, ty at which
471
+ // the ellipse has the closest point to the given point
472
+ [0, 1, 2, 3].forEach((_) => {
473
+ const xx = a * tx;
474
+ const yy = b * ty;
475
+ const ex = ((a * a - b * b) * tx ** 3) / a;
476
+ const ey = ((b * b - a * a) * ty ** 3) / b;
477
+ const rx = xx - ex;
478
+ const ry = yy - ey;
479
+ const qx = px - ex;
480
+ const qy = py - ey;
481
+ const r = Math.hypot(ry, rx);
482
+ const q = Math.hypot(qy, qx);
483
+ tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
484
+ ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
485
+ const t = Math.hypot(ty, tx);
486
+ tx /= t;
487
+ ty /= t;
488
+ });
489
+ const closestPoint = GA.point(a * tx, b * ty);
490
+ const tangent = GALine.orthogonalThrough(pointRel, closestPoint);
491
+ return [pointRel, tangent];
492
+ };
493
+ // Returns:
494
+ // 1. the point relative to the elements (x, y) position
495
+ // 2. the point relative to the element's center with positive (x, y)
496
+ // 3. half element width
497
+ // 4. half element height
498
+ //
499
+ // Note that for linear elements the (x, y) position is not at the
500
+ // top right corner of their boundary.
501
+ //
502
+ // Rectangles, diamonds and ellipses are symmetrical over axes,
503
+ // and other elements have a rectangular boundary,
504
+ // so we only need to perform hit tests for the positive quadrant.
505
+ const pointRelativeToElement = (element, pointTuple, elementsMap) => {
506
+ const point = GAPoint.from(pointTuple);
507
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
508
+ const center = coordsCenter(x1, y1, x2, y2);
509
+ // GA has angle orientation opposite to `rotate`
510
+ const rotate = GATransform.rotation(center, element.angle);
511
+ const pointRotated = GATransform.apply(rotate, point);
512
+ const pointRelToCenter = GA.sub(pointRotated, GADirection.from(center));
513
+ const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
514
+ const elementPos = GA.offset(element.x, element.y);
515
+ const pointRelToPos = GA.sub(pointRotated, elementPos);
516
+ const halfWidth = (x2 - x1) / 2;
517
+ const halfHeight = (y2 - y1) / 2;
518
+ return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
519
+ };
520
+ const relativizationToElementCenter = (element, elementsMap) => {
521
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
522
+ const center = coordsCenter(x1, y1, x2, y2);
523
+ // GA has angle orientation opposite to `rotate`
524
+ const rotate = GATransform.rotation(center, element.angle);
525
+ const translate = GA.reverse(GATransform.translation(GADirection.from(center)));
526
+ return GATransform.compose(rotate, translate);
527
+ };
528
+ const coordsCenter = (x1, y1, x2, y2) => {
529
+ return GA.point((x1 + x2) / 2, (y1 + y2) / 2);
530
+ };
531
+ // The focus distance is the oriented ratio between the size of
532
+ // the `element` and the "focus image" of the element on which
533
+ // all focus points lie, so it's a number between -1 and 1.
534
+ // The line going through `a` and `b` is a tangent to the "focus image"
535
+ // of the element.
536
+ export const determineFocusDistance = (element,
537
+ // Point on the line, in absolute coordinates
538
+ a,
539
+ // Another point on the line, in absolute coordinates (closer to element)
540
+ b, elementsMap) => {
541
+ const relateToCenter = relativizationToElementCenter(element, elementsMap);
542
+ const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
543
+ const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
544
+ const line = GALine.through(aRel, bRel);
545
+ const q = element.height / element.width;
546
+ const hwidth = element.width / 2;
547
+ const hheight = element.height / 2;
548
+ const n = line[2];
549
+ const m = line[3];
550
+ const c = line[1];
551
+ const mabs = Math.abs(m);
552
+ const nabs = Math.abs(n);
553
+ let ret;
554
+ switch (element.type) {
555
+ case "rectangle":
556
+ case "image":
557
+ case "text":
558
+ case "iframe":
559
+ case "embeddable":
560
+ case "frame":
561
+ case "magicframe":
562
+ ret = c / (hwidth * (nabs + q * mabs));
563
+ break;
564
+ case "diamond":
565
+ ret = mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
566
+ break;
567
+ case "ellipse":
568
+ ret = c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
569
+ break;
570
+ }
571
+ return ret || 0;
572
+ };
573
+ export const determineFocusPoint = (element,
574
+ // The oriented, relative distance from the center of `element` of the
575
+ // returned focusPoint
576
+ focus, adjecentPoint, elementsMap) => {
577
+ if (focus === 0) {
578
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
579
+ const center = coordsCenter(x1, y1, x2, y2);
580
+ return GAPoint.toTuple(center);
581
+ }
582
+ const relateToCenter = relativizationToElementCenter(element, elementsMap);
583
+ const adjecentPointRel = GATransform.apply(relateToCenter, GAPoint.from(adjecentPoint));
584
+ const reverseRelateToCenter = GA.reverse(relateToCenter);
585
+ let point;
586
+ switch (element.type) {
587
+ case "rectangle":
588
+ case "image":
589
+ case "text":
590
+ case "diamond":
591
+ case "iframe":
592
+ case "embeddable":
593
+ case "frame":
594
+ case "magicframe":
595
+ point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
596
+ break;
597
+ case "ellipse":
598
+ point = findFocusPointForEllipse(element, focus, adjecentPointRel);
599
+ break;
600
+ }
601
+ return GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point));
602
+ };
603
+ // Returns 2 or 0 intersection points between line going through `a` and `b`
604
+ // and the `element`, in ascending order of distance from `a`.
605
+ export const intersectElementWithLine = (element,
606
+ // Point on the line, in absolute coordinates
607
+ a,
608
+ // Another point on the line, in absolute coordinates
609
+ b,
610
+ // If given, the element is inflated by this value
611
+ gap = 0, elementsMap) => {
612
+ const relateToCenter = relativizationToElementCenter(element, elementsMap);
613
+ const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
614
+ const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
615
+ const line = GALine.through(aRel, bRel);
616
+ const reverseRelateToCenter = GA.reverse(relateToCenter);
617
+ const intersections = getSortedElementLineIntersections(element, line, aRel, gap);
618
+ return intersections.map((point) => GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)));
619
+ };
620
+ const getSortedElementLineIntersections = (element,
621
+ // Relative to element center
622
+ line,
623
+ // Relative to element center
624
+ nearPoint, gap = 0) => {
625
+ let intersections;
626
+ switch (element.type) {
627
+ case "rectangle":
628
+ case "image":
629
+ case "text":
630
+ case "diamond":
631
+ case "iframe":
632
+ case "embeddable":
633
+ case "frame":
634
+ case "magicframe":
635
+ const corners = getCorners(element);
636
+ intersections = corners
637
+ .flatMap((point, i) => {
638
+ const edge = [point, corners[(i + 1) % 4]];
639
+ return intersectSegment(line, offsetSegment(edge, gap));
640
+ })
641
+ .concat(corners.flatMap((point) => getCircleIntersections(point, gap, line)));
642
+ break;
643
+ case "ellipse":
644
+ intersections = getEllipseIntersections(element, gap, line);
645
+ break;
646
+ }
647
+ if (intersections.length < 2) {
648
+ // Ignore the "edge" case of only intersecting with a single corner
649
+ return [];
650
+ }
651
+ const sortedIntersections = intersections.sort((i1, i2) => GAPoint.distance(i1, nearPoint) - GAPoint.distance(i2, nearPoint));
652
+ return [
653
+ sortedIntersections[0],
654
+ sortedIntersections[sortedIntersections.length - 1],
655
+ ];
656
+ };
657
+ const getCorners = (element, scale = 1) => {
658
+ const hx = (scale * element.width) / 2;
659
+ const hy = (scale * element.height) / 2;
660
+ switch (element.type) {
661
+ case "rectangle":
662
+ case "image":
663
+ case "text":
664
+ case "iframe":
665
+ case "embeddable":
666
+ case "frame":
667
+ case "magicframe":
668
+ return [
669
+ GA.point(hx, hy),
670
+ GA.point(hx, -hy),
671
+ GA.point(-hx, -hy),
672
+ GA.point(-hx, hy),
673
+ ];
674
+ case "diamond":
675
+ return [
676
+ GA.point(0, hy),
677
+ GA.point(hx, 0),
678
+ GA.point(0, -hy),
679
+ GA.point(-hx, 0),
680
+ ];
681
+ }
682
+ };
683
+ // Returns intersection of `line` with `segment`, with `segment` moved by
684
+ // `gap` in its polar direction.
685
+ // If intersection coincides with second segment point returns empty array.
686
+ const intersectSegment = (line, segment) => {
687
+ const [a, b] = segment;
688
+ const aDist = GAPoint.distanceToLine(a, line);
689
+ const bDist = GAPoint.distanceToLine(b, line);
690
+ if (aDist * bDist >= 0) {
691
+ // The intersection is outside segment `(a, b)`
692
+ return [];
693
+ }
694
+ return [GAPoint.intersect(line, GALine.through(a, b))];
695
+ };
696
+ const offsetSegment = (segment, distance) => {
697
+ const [a, b] = segment;
698
+ const offset = GATransform.translationOrthogonal(GADirection.fromTo(a, b), distance);
699
+ return [GATransform.apply(offset, a), GATransform.apply(offset, b)];
700
+ };
701
+ const getEllipseIntersections = (element, gap, line) => {
702
+ const a = element.width / 2 + gap;
703
+ const b = element.height / 2 + gap;
704
+ const m = line[2];
705
+ const n = line[3];
706
+ const c = line[1];
707
+ const squares = a * a * m * m + b * b * n * n;
708
+ const discr = squares - c * c;
709
+ if (squares === 0 || discr <= 0) {
710
+ return [];
711
+ }
712
+ const discrRoot = Math.sqrt(discr);
713
+ const xn = -a * a * m * c;
714
+ const yn = -b * b * n * c;
715
+ return [
716
+ GA.point((xn + a * b * n * discrRoot) / squares, (yn - a * b * m * discrRoot) / squares),
717
+ GA.point((xn - a * b * n * discrRoot) / squares, (yn + a * b * m * discrRoot) / squares),
718
+ ];
719
+ };
720
+ export const getCircleIntersections = (center, radius, line) => {
721
+ if (radius === 0) {
722
+ return GAPoint.distanceToLine(line, center) === 0 ? [center] : [];
723
+ }
724
+ const m = line[2];
725
+ const n = line[3];
726
+ const c = line[1];
727
+ const [a, b] = GAPoint.toTuple(center);
728
+ const r = radius;
729
+ const squares = m * m + n * n;
730
+ const discr = r * r * squares - (m * a + n * b + c) ** 2;
731
+ if (squares === 0 || discr <= 0) {
732
+ return [];
733
+ }
734
+ const discrRoot = Math.sqrt(discr);
735
+ const xn = a * n * n - b * m * n - m * c;
736
+ const yn = b * m * m - a * m * n - n * c;
737
+ return [
738
+ GA.point((xn + n * discrRoot) / squares, (yn - m * discrRoot) / squares),
739
+ GA.point((xn - n * discrRoot) / squares, (yn + m * discrRoot) / squares),
740
+ ];
741
+ };
742
+ // The focus point is the tangent point of the "focus image" of the
743
+ // `element`, where the tangent goes through `point`.
744
+ export const findFocusPointForEllipse = (ellipse,
745
+ // Between -1 and 1 (not 0) the relative size of the "focus image" of
746
+ // the element on which the focus point lies
747
+ relativeDistance,
748
+ // The point for which we're trying to find the focus point, relative
749
+ // to the ellipse center.
750
+ point) => {
751
+ const relativeDistanceAbs = Math.abs(relativeDistance);
752
+ const a = (ellipse.width * relativeDistanceAbs) / 2;
753
+ const b = (ellipse.height * relativeDistanceAbs) / 2;
754
+ const orientation = Math.sign(relativeDistance);
755
+ const [px, pyo] = GAPoint.toTuple(point);
756
+ // The calculation below can't handle py = 0
757
+ const py = pyo === 0 ? 0.0001 : pyo;
758
+ const squares = px ** 2 * b ** 2 + py ** 2 * a ** 2;
759
+ // Tangent mx + ny + 1 = 0
760
+ const m = (-px * b ** 2 +
761
+ orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
762
+ squares;
763
+ let n = (-m * px - 1) / py;
764
+ if (n === 0) {
765
+ // if zero {-0, 0}, fall back to a same-sign value in the similar range
766
+ n = (Object.is(n, -0) ? -1 : 1) * 0.01;
767
+ }
768
+ const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
769
+ return GA.point(x, (-m * x - 1) / n);
770
+ };
771
+ export const findFocusPointForRectangulars = (element,
772
+ // Between -1 and 1 for how far away should the focus point be relative
773
+ // to the size of the element. Sign determines orientation.
774
+ relativeDistance,
775
+ // The point for which we're trying to find the focus point, relative
776
+ // to the element center.
777
+ point) => {
778
+ const relativeDistanceAbs = Math.abs(relativeDistance);
779
+ const orientation = Math.sign(relativeDistance);
780
+ const corners = getCorners(element, relativeDistanceAbs);
781
+ let maxDistance = 0;
782
+ let tangentPoint = null;
783
+ corners.forEach((corner) => {
784
+ const distance = orientation * GALine.through(point, corner)[1];
785
+ if (distance > maxDistance) {
786
+ maxDistance = distance;
787
+ tangentPoint = corner;
788
+ }
789
+ });
790
+ return tangentPoint;
791
+ };
@@ -36,7 +36,6 @@ export declare const getElementLineSegments: (element: ExcalidrawElement, elemen
36
36
  * Rectangle here means any rectangular frame, not an excalidraw element.
37
37
  */
38
38
  export declare const getRectangleBoxAbsoluteCoords: (boxSceneCoords: RectangleBox) => number[];
39
- export declare const pointRelativeTo: (element: ExcalidrawElement, absoluteCoords: readonly [number, number]) => readonly [number, number];
40
39
  export declare const getDiamondPoints: (element: ExcalidrawElement) => number[];
41
40
  export declare const getCurvePathOps: (shape: Drawable) => Op[];
42
41
  export declare const getMinMaxXYFromCurvePathOps: (ops: Op[], transformXY?: ((x: number, y: number) => [number, number]) | undefined) => Bounds;
@@ -192,9 +192,6 @@ export const getRectangleBoxAbsoluteCoords = (boxSceneCoords) => {
192
192
  boxSceneCoords.y + boxSceneCoords.height / 2,
193
193
  ];
194
194
  };
195
- export const pointRelativeTo = (element, absoluteCoords) => {
196
- return [absoluteCoords[0] - element.x, absoluteCoords[1] - element.y];
197
- };
198
195
  export const getDiamondPoints = (element) => {
199
196
  // Here we add +1 to avoid these numbers to be 0
200
197
  // otherwise rough.js will throw an error complaining about it
@@ -1,21 +1,16 @@
1
- import * as GA from "../ga";
2
- import { NonDeletedExcalidrawElement, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawRectangleElement, ExcalidrawDiamondElement, ExcalidrawTextElement, ExcalidrawEllipseElement, NonDeleted, ExcalidrawImageElement, ExcalidrawFrameLikeElement, ExcalidrawIframeLikeElement, ElementsMap } from "./types";
3
- import { FrameNameBoundsCache, Point } from "../types";
4
- import { AppState } from "../types";
5
- export declare const hitTest: (element: NonDeletedExcalidrawElement, appState: AppState, frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, elementsMap: ElementsMap) => boolean;
6
- export declare const isHittingElementBoundingBoxWithoutHittingElement: (element: NonDeletedExcalidrawElement, appState: AppState, frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, elementsMap: ElementsMap) => boolean;
7
- export declare const isHittingElementNotConsideringBoundingBox: (element: NonDeletedExcalidrawElement, appState: AppState, frameNameBoundsCache: FrameNameBoundsCache | null, point: readonly [number, number], elementsMap: ElementsMap) => boolean;
8
- export declare const isPointHittingElementBoundingBox: (element: NonDeleted<ExcalidrawElement>, elementsMap: ElementsMap, [x, y]: readonly [number, number], threshold: number, frameNameBoundsCache: FrameNameBoundsCache | null) => boolean;
9
- export declare const bindingBorderTest: (element: NonDeleted<ExcalidrawBindableElement>, { x, y }: {
1
+ import { ElementsMap, ExcalidrawElement } from "./types";
2
+ import { FrameNameBounds } from "../types";
3
+ import { GeometricShape } from "../../utils/geometry/shape";
4
+ export declare const shouldTestInside: (element: ExcalidrawElement) => boolean;
5
+ export type HitTestArgs = {
10
6
  x: number;
11
7
  y: number;
12
- }, elementsMap: ElementsMap) => boolean;
13
- export declare const maxBindingGap: (element: ExcalidrawElement, elementWidth: number, elementHeight: number) => number;
14
- export declare const distanceToBindableElement: (element: ExcalidrawBindableElement, point: readonly [number, number], elementsMap: ElementsMap) => number;
15
- export declare const pointInAbsoluteCoords: (element: ExcalidrawElement, elementsMap: ElementsMap, point: readonly [number, number]) => readonly [number, number];
16
- export declare const determineFocusDistance: (element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], elementsMap: ElementsMap) => number;
17
- export declare const determineFocusPoint: (element: ExcalidrawBindableElement, focus: number, adjecentPoint: readonly [number, number], elementsMap: ElementsMap) => readonly [number, number];
18
- export declare const intersectElementWithLine: (element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap: number | undefined, elementsMap: ElementsMap) => Point[];
19
- export declare const getCircleIntersections: (center: readonly [number, number, number, number, number, number, number, number], radius: number, line: readonly [number, number, number, number, number, number, number, number]) => GA.Point[];
20
- export declare const findFocusPointForEllipse: (ellipse: ExcalidrawEllipseElement, relativeDistance: number, point: readonly [number, number, number, number, number, number, number, number]) => readonly [number, number, number, number, number, number, number, number];
21
- export declare const findFocusPointForRectangulars: (element: ExcalidrawRectangleElement | ExcalidrawImageElement | ExcalidrawDiamondElement | ExcalidrawTextElement | ExcalidrawIframeLikeElement | ExcalidrawFrameLikeElement, relativeDistance: number, point: readonly [number, number, number, number, number, number, number, number]) => readonly [number, number, number, number, number, number, number, number];
8
+ element: ExcalidrawElement;
9
+ shape: GeometricShape;
10
+ threshold?: number;
11
+ frameNameBound?: FrameNameBounds | null;
12
+ };
13
+ export declare const hitElementItself: ({ x, y, element, shape, threshold, frameNameBound, }: HitTestArgs) => boolean;
14
+ export declare const hitElementBoundingBox: (x: number, y: number, element: ExcalidrawElement, elementsMap: ElementsMap, tolerance?: number) => boolean;
15
+ export declare const hitElementBoundingBoxOnly: (hitArgs: HitTestArgs, elementsMap: ElementsMap) => boolean;
16
+ export declare const hitElementBoundText: (x: number, y: number, textShape: GeometricShape | null) => boolean | null;