binding.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. import {
  2. ExcalidrawLinearElement,
  3. ExcalidrawBindableElement,
  4. NonDeleted,
  5. NonDeletedExcalidrawElement,
  6. PointBinding,
  7. ExcalidrawElement,
  8. } from "./types";
  9. import { getElementAtPosition } from "../scene";
  10. import { AppState } from "../types";
  11. import { isBindableElement, isBindingElement } from "./typeChecks";
  12. import {
  13. bindingBorderTest,
  14. distanceToBindableElement,
  15. maxBindingGap,
  16. determineFocusDistance,
  17. intersectElementWithLine,
  18. determineFocusPoint,
  19. } from "./collision";
  20. import { mutateElement } from "./mutateElement";
  21. import Scene from "../scene/Scene";
  22. import { LinearElementEditor } from "./linearElementEditor";
  23. import { tupleToCoors } from "../utils";
  24. import { KEYS } from "../keys";
  25. export type SuggestedBinding =
  26. | NonDeleted<ExcalidrawBindableElement>
  27. | SuggestedPointBinding;
  28. export type SuggestedPointBinding = [
  29. NonDeleted<ExcalidrawLinearElement>,
  30. "start" | "end" | "both",
  31. NonDeleted<ExcalidrawBindableElement>,
  32. ];
  33. export const shouldEnableBindingForPointerEvent = (
  34. event: React.PointerEvent<HTMLCanvasElement>,
  35. ) => {
  36. return !event[KEYS.CTRL_OR_CMD];
  37. };
  38. export const isBindingEnabled = (appState: AppState): boolean => {
  39. return appState.isBindingEnabled;
  40. };
  41. export const bindOrUnbindLinearElement = (
  42. linearElement: NonDeleted<ExcalidrawLinearElement>,
  43. startBindingElement: ExcalidrawBindableElement | null | "keep",
  44. endBindingElement: ExcalidrawBindableElement | null | "keep",
  45. ): void => {
  46. const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
  47. const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
  48. bindOrUnbindLinearElementEdge(
  49. linearElement,
  50. startBindingElement,
  51. endBindingElement,
  52. "start",
  53. boundToElementIds,
  54. unboundFromElementIds,
  55. );
  56. bindOrUnbindLinearElementEdge(
  57. linearElement,
  58. endBindingElement,
  59. startBindingElement,
  60. "end",
  61. boundToElementIds,
  62. unboundFromElementIds,
  63. );
  64. const onlyUnbound = Array.from(unboundFromElementIds).filter(
  65. (id) => !boundToElementIds.has(id),
  66. );
  67. Scene.getScene(linearElement)!
  68. .getNonDeletedElements(onlyUnbound)
  69. .forEach((element) => {
  70. mutateElement(element, {
  71. boundElementIds: element.boundElementIds?.filter(
  72. (id) => id !== linearElement.id,
  73. ),
  74. });
  75. });
  76. };
  77. const bindOrUnbindLinearElementEdge = (
  78. linearElement: NonDeleted<ExcalidrawLinearElement>,
  79. bindableElement: ExcalidrawBindableElement | null | "keep",
  80. otherEdgeBindableElement: ExcalidrawBindableElement | null | "keep",
  81. startOrEnd: "start" | "end",
  82. // Is mutated
  83. boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
  84. // Is mutated
  85. unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
  86. ): void => {
  87. if (bindableElement !== "keep") {
  88. if (bindableElement != null) {
  89. // Don't bind if we're trying to bind or are already bound to the same
  90. // element on the other edge already ("start" edge takes precedence).
  91. if (
  92. otherEdgeBindableElement == null ||
  93. (otherEdgeBindableElement === "keep"
  94. ? !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
  95. linearElement,
  96. bindableElement,
  97. startOrEnd,
  98. )
  99. : startOrEnd === "start" ||
  100. otherEdgeBindableElement.id !== bindableElement.id)
  101. ) {
  102. bindLinearElement(linearElement, bindableElement, startOrEnd);
  103. boundToElementIds.add(bindableElement.id);
  104. }
  105. } else {
  106. const unbound = unbindLinearElement(linearElement, startOrEnd);
  107. if (unbound != null) {
  108. unboundFromElementIds.add(unbound);
  109. }
  110. }
  111. }
  112. };
  113. export const bindOrUnbindSelectedElements = (
  114. elements: NonDeleted<ExcalidrawElement>[],
  115. ): void => {
  116. elements.forEach((element) => {
  117. if (isBindingElement(element)) {
  118. bindOrUnbindLinearElement(
  119. element,
  120. getElligibleElementForBindingElement(element, "start"),
  121. getElligibleElementForBindingElement(element, "end"),
  122. );
  123. } else if (isBindableElement(element)) {
  124. maybeBindBindableElement(element);
  125. }
  126. });
  127. };
  128. const maybeBindBindableElement = (
  129. bindableElement: NonDeleted<ExcalidrawBindableElement>,
  130. ): void => {
  131. getElligibleElementsForBindableElementAndWhere(
  132. bindableElement,
  133. ).forEach(([linearElement, where]) =>
  134. bindOrUnbindLinearElement(
  135. linearElement,
  136. where === "end" ? "keep" : bindableElement,
  137. where === "start" ? "keep" : bindableElement,
  138. ),
  139. );
  140. };
  141. export const maybeBindLinearElement = (
  142. linearElement: NonDeleted<ExcalidrawLinearElement>,
  143. appState: AppState,
  144. scene: Scene,
  145. pointerCoords: { x: number; y: number },
  146. ): void => {
  147. if (appState.startBoundElement != null) {
  148. bindLinearElement(linearElement, appState.startBoundElement, "start");
  149. }
  150. const hoveredElement = getHoveredElementForBinding(pointerCoords, scene);
  151. if (
  152. hoveredElement != null &&
  153. !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
  154. linearElement,
  155. hoveredElement,
  156. "end",
  157. )
  158. ) {
  159. bindLinearElement(linearElement, hoveredElement, "end");
  160. }
  161. };
  162. const bindLinearElement = (
  163. linearElement: NonDeleted<ExcalidrawLinearElement>,
  164. hoveredElement: ExcalidrawBindableElement,
  165. startOrEnd: "start" | "end",
  166. ): void => {
  167. mutateElement(linearElement, {
  168. [startOrEnd === "start" ? "startBinding" : "endBinding"]: {
  169. elementId: hoveredElement.id,
  170. ...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd),
  171. } as PointBinding,
  172. });
  173. mutateElement(hoveredElement, {
  174. boundElementIds: Array.from(
  175. new Set([...(hoveredElement.boundElementIds ?? []), linearElement.id]),
  176. ),
  177. });
  178. };
  179. // Don't bind both ends of a simple segment
  180. const isLinearElementSimpleAndAlreadyBoundOnOppositeEdge = (
  181. linearElement: NonDeleted<ExcalidrawLinearElement>,
  182. bindableElement: ExcalidrawBindableElement,
  183. startOrEnd: "start" | "end",
  184. ): boolean => {
  185. const otherBinding =
  186. linearElement[startOrEnd === "start" ? "endBinding" : "startBinding"];
  187. return isLinearElementSimpleAndAlreadyBound(
  188. linearElement,
  189. otherBinding?.elementId,
  190. bindableElement,
  191. );
  192. };
  193. export const isLinearElementSimpleAndAlreadyBound = (
  194. linearElement: NonDeleted<ExcalidrawLinearElement>,
  195. alreadyBoundToId: ExcalidrawBindableElement["id"] | undefined,
  196. bindableElement: ExcalidrawBindableElement,
  197. ): boolean => {
  198. return (
  199. alreadyBoundToId === bindableElement.id && linearElement.points.length < 3
  200. );
  201. };
  202. export const unbindLinearElements = (
  203. elements: NonDeleted<ExcalidrawElement>[],
  204. ): void => {
  205. elements.forEach((element) => {
  206. if (isBindingElement(element)) {
  207. bindOrUnbindLinearElement(element, null, null);
  208. }
  209. });
  210. };
  211. const unbindLinearElement = (
  212. linearElement: NonDeleted<ExcalidrawLinearElement>,
  213. startOrEnd: "start" | "end",
  214. ): ExcalidrawBindableElement["id"] | null => {
  215. const field = startOrEnd === "start" ? "startBinding" : "endBinding";
  216. const binding = linearElement[field];
  217. if (binding == null) {
  218. return null;
  219. }
  220. mutateElement(linearElement, { [field]: null });
  221. return binding.elementId;
  222. };
  223. export const getHoveredElementForBinding = (
  224. pointerCoords: {
  225. x: number;
  226. y: number;
  227. },
  228. scene: Scene,
  229. ): NonDeleted<ExcalidrawBindableElement> | null => {
  230. const hoveredElement = getElementAtPosition(
  231. scene.getElements(),
  232. (element) =>
  233. isBindableElement(element) && bindingBorderTest(element, pointerCoords),
  234. );
  235. return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
  236. };
  237. const calculateFocusAndGap = (
  238. linearElement: NonDeleted<ExcalidrawLinearElement>,
  239. hoveredElement: ExcalidrawBindableElement,
  240. startOrEnd: "start" | "end",
  241. ): { focus: number; gap: number } => {
  242. const direction = startOrEnd === "start" ? -1 : 1;
  243. const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
  244. const adjacentPointIndex = edgePointIndex - direction;
  245. const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
  246. linearElement,
  247. edgePointIndex,
  248. );
  249. const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
  250. linearElement,
  251. adjacentPointIndex,
  252. );
  253. return {
  254. focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
  255. gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
  256. };
  257. };
  258. // Supports translating, rotating and scaling `changedElement` with bound
  259. // linear elements.
  260. // Because scaling involves moving the focus points as well, it is
  261. // done before the `changedElement` is updated, and the `newSize` is passed
  262. // in explicitly.
  263. export const updateBoundElements = (
  264. changedElement: NonDeletedExcalidrawElement,
  265. options?: {
  266. simultaneouslyUpdated?: readonly ExcalidrawElement[];
  267. newSize?: { width: number; height: number };
  268. },
  269. ) => {
  270. const boundElementIds = changedElement.boundElementIds ?? [];
  271. if (boundElementIds.length === 0) {
  272. return;
  273. }
  274. const { newSize, simultaneouslyUpdated } = options ?? {};
  275. const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
  276. simultaneouslyUpdated,
  277. );
  278. (Scene.getScene(changedElement)!.getNonDeletedElements(
  279. boundElementIds,
  280. ) as NonDeleted<ExcalidrawLinearElement>[]).forEach((linearElement) => {
  281. const bindableElement = changedElement as ExcalidrawBindableElement;
  282. // In case the boundElementIds are stale
  283. if (!doesNeedUpdate(linearElement, bindableElement)) {
  284. return;
  285. }
  286. const startBinding = maybeCalculateNewGapWhenScaling(
  287. bindableElement,
  288. linearElement.startBinding,
  289. newSize,
  290. );
  291. const endBinding = maybeCalculateNewGapWhenScaling(
  292. bindableElement,
  293. linearElement.endBinding,
  294. newSize,
  295. );
  296. // `linearElement` is being moved/scaled already, just update the binding
  297. if (simultaneouslyUpdatedElementIds.has(linearElement.id)) {
  298. mutateElement(linearElement, { startBinding, endBinding });
  299. return;
  300. }
  301. updateBoundPoint(
  302. linearElement,
  303. "start",
  304. startBinding,
  305. changedElement as ExcalidrawBindableElement,
  306. );
  307. updateBoundPoint(
  308. linearElement,
  309. "end",
  310. endBinding,
  311. changedElement as ExcalidrawBindableElement,
  312. );
  313. });
  314. };
  315. const doesNeedUpdate = (
  316. boundElement: NonDeleted<ExcalidrawLinearElement>,
  317. changedElement: ExcalidrawBindableElement,
  318. ) => {
  319. return (
  320. boundElement.startBinding?.elementId === changedElement.id ||
  321. boundElement.endBinding?.elementId === changedElement.id
  322. );
  323. };
  324. const getSimultaneouslyUpdatedElementIds = (
  325. simultaneouslyUpdated: readonly ExcalidrawElement[] | undefined,
  326. ): Set<ExcalidrawElement["id"]> => {
  327. return new Set((simultaneouslyUpdated || []).map((element) => element.id));
  328. };
  329. const updateBoundPoint = (
  330. linearElement: NonDeleted<ExcalidrawLinearElement>,
  331. startOrEnd: "start" | "end",
  332. binding: PointBinding | null | undefined,
  333. changedElement: ExcalidrawBindableElement,
  334. ): void => {
  335. if (
  336. binding == null ||
  337. // We only need to update the other end if this is a 2 point line element
  338. (binding.elementId !== changedElement.id && linearElement.points.length > 2)
  339. ) {
  340. return;
  341. }
  342. const bindingElement = Scene.getScene(linearElement)!.getElement(
  343. binding.elementId,
  344. ) as ExcalidrawBindableElement | null;
  345. if (bindingElement == null) {
  346. // We're not cleaning up after deleted elements atm., so handle this case
  347. return;
  348. }
  349. const direction = startOrEnd === "start" ? -1 : 1;
  350. const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
  351. const adjacentPointIndex = edgePointIndex - direction;
  352. const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
  353. linearElement,
  354. adjacentPointIndex,
  355. );
  356. const focusPointAbsolute = determineFocusPoint(
  357. bindingElement,
  358. binding.focus,
  359. adjacentPoint,
  360. );
  361. let newEdgePoint;
  362. // The linear element was not originally pointing inside the bound shape,
  363. // we can point directly at the focus point
  364. if (binding.gap === 0) {
  365. newEdgePoint = focusPointAbsolute;
  366. } else {
  367. const intersections = intersectElementWithLine(
  368. bindingElement,
  369. adjacentPoint,
  370. focusPointAbsolute,
  371. binding.gap,
  372. );
  373. if (intersections.length === 0) {
  374. // This should never happen, since focusPoint should always be
  375. // inside the element, but just in case, bail out
  376. newEdgePoint = focusPointAbsolute;
  377. } else {
  378. // Guaranteed to intersect because focusPoint is always inside the shape
  379. newEdgePoint = intersections[0];
  380. }
  381. }
  382. LinearElementEditor.movePoint(
  383. linearElement,
  384. edgePointIndex,
  385. LinearElementEditor.pointFromAbsoluteCoords(linearElement, newEdgePoint),
  386. { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding },
  387. );
  388. };
  389. const maybeCalculateNewGapWhenScaling = (
  390. changedElement: ExcalidrawBindableElement,
  391. currentBinding: PointBinding | null | undefined,
  392. newSize: { width: number; height: number } | undefined,
  393. ): PointBinding | null | undefined => {
  394. if (currentBinding == null || newSize == null) {
  395. return currentBinding;
  396. }
  397. const { gap, focus, elementId } = currentBinding;
  398. const { width: newWidth, height: newHeight } = newSize;
  399. const { width, height } = changedElement;
  400. const newGap = Math.max(
  401. 1,
  402. Math.min(
  403. maxBindingGap(changedElement, newWidth, newHeight),
  404. gap * (newWidth < newHeight ? newWidth / width : newHeight / height),
  405. ),
  406. );
  407. return { elementId, gap: newGap, focus };
  408. };
  409. export const getEligibleElementsForBinding = (
  410. elements: NonDeleted<ExcalidrawElement>[],
  411. ): SuggestedBinding[] => {
  412. const includedElementIds = new Set(elements.map(({ id }) => id));
  413. return elements.flatMap((element) =>
  414. isBindingElement(element)
  415. ? (getElligibleElementsForBindingElement(
  416. element as NonDeleted<ExcalidrawLinearElement>,
  417. ).filter(
  418. (element) => !includedElementIds.has(element.id),
  419. ) as SuggestedBinding[])
  420. : isBindableElement(element)
  421. ? getElligibleElementsForBindableElementAndWhere(element).filter(
  422. (binding) => !includedElementIds.has(binding[0].id),
  423. )
  424. : [],
  425. );
  426. };
  427. const getElligibleElementsForBindingElement = (
  428. linearElement: NonDeleted<ExcalidrawLinearElement>,
  429. ): NonDeleted<ExcalidrawBindableElement>[] => {
  430. return [
  431. getElligibleElementForBindingElement(linearElement, "start"),
  432. getElligibleElementForBindingElement(linearElement, "end"),
  433. ].filter(
  434. (element): element is NonDeleted<ExcalidrawBindableElement> =>
  435. element != null,
  436. );
  437. };
  438. const getElligibleElementForBindingElement = (
  439. linearElement: NonDeleted<ExcalidrawLinearElement>,
  440. startOrEnd: "start" | "end",
  441. ): NonDeleted<ExcalidrawBindableElement> | null => {
  442. return getHoveredElementForBinding(
  443. getLinearElementEdgeCoors(linearElement, startOrEnd),
  444. Scene.getScene(linearElement)!,
  445. );
  446. };
  447. const getLinearElementEdgeCoors = (
  448. linearElement: NonDeleted<ExcalidrawLinearElement>,
  449. startOrEnd: "start" | "end",
  450. ): { x: number; y: number } => {
  451. const index = startOrEnd === "start" ? 0 : -1;
  452. return tupleToCoors(
  453. LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index),
  454. );
  455. };
  456. const getElligibleElementsForBindableElementAndWhere = (
  457. bindableElement: NonDeleted<ExcalidrawBindableElement>,
  458. ): SuggestedPointBinding[] => {
  459. return Scene.getScene(bindableElement)!
  460. .getElements()
  461. .map((element) => {
  462. if (!isBindingElement(element)) {
  463. return null;
  464. }
  465. const canBindStart = isLinearElementEligibleForNewBindingByBindable(
  466. element,
  467. "start",
  468. bindableElement,
  469. );
  470. const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
  471. element,
  472. "end",
  473. bindableElement,
  474. );
  475. if (!canBindStart && !canBindEnd) {
  476. return null;
  477. }
  478. return [
  479. element,
  480. canBindStart && canBindEnd ? "both" : canBindStart ? "start" : "end",
  481. bindableElement,
  482. ];
  483. })
  484. .filter((maybeElement) => maybeElement != null) as SuggestedPointBinding[];
  485. };
  486. const isLinearElementEligibleForNewBindingByBindable = (
  487. linearElement: NonDeleted<ExcalidrawLinearElement>,
  488. startOrEnd: "start" | "end",
  489. bindableElement: NonDeleted<ExcalidrawBindableElement>,
  490. ): boolean => {
  491. const existingBinding =
  492. linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
  493. return (
  494. existingBinding == null &&
  495. !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
  496. linearElement,
  497. bindableElement,
  498. startOrEnd,
  499. ) &&
  500. bindingBorderTest(
  501. bindableElement,
  502. getLinearElementEdgeCoors(linearElement, startOrEnd),
  503. )
  504. );
  505. };
  506. // We need to:
  507. // 1: Update elements not selected to point to duplicated elements
  508. // 2: Update duplicated elements to point to other duplicated elements
  509. export const fixBindingsAfterDuplication = (
  510. sceneElements: readonly ExcalidrawElement[],
  511. oldElements: readonly ExcalidrawElement[],
  512. oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
  513. // There are three copying mechanisms: Copy-paste, duplication and alt-drag.
  514. // Only when alt-dragging the new "duplicates" act as the "old", while
  515. // the "old" elements act as the "new copy" - essentially working reverse
  516. // to the other two.
  517. duplicatesServeAsOld?: "duplicatesServeAsOld" | undefined,
  518. ): void => {
  519. // First collect all the binding/bindable elements, so we only update
  520. // each once, regardless of whether they were duplicated or not.
  521. const allBoundElementIds: Set<ExcalidrawElement["id"]> = new Set();
  522. const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
  523. const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
  524. oldElements.forEach((oldElement) => {
  525. const { boundElementIds } = oldElement;
  526. if (boundElementIds != null && boundElementIds.length > 0) {
  527. boundElementIds.forEach((boundElementId) => {
  528. if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElementId)) {
  529. allBoundElementIds.add(boundElementId);
  530. }
  531. });
  532. allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
  533. }
  534. if (isBindingElement(oldElement)) {
  535. if (oldElement.startBinding != null) {
  536. const { elementId } = oldElement.startBinding;
  537. if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
  538. allBindableElementIds.add(elementId);
  539. }
  540. }
  541. if (oldElement.endBinding != null) {
  542. const { elementId } = oldElement.endBinding;
  543. if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
  544. allBindableElementIds.add(elementId);
  545. }
  546. }
  547. if (oldElement.startBinding != null || oldElement.endBinding != null) {
  548. allBoundElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
  549. }
  550. }
  551. });
  552. // Update the linear elements
  553. (sceneElements.filter(({ id }) =>
  554. allBoundElementIds.has(id),
  555. ) as ExcalidrawLinearElement[]).forEach((element) => {
  556. const { startBinding, endBinding } = element;
  557. mutateElement(element, {
  558. startBinding: newBindingAfterDuplication(
  559. startBinding,
  560. oldIdToDuplicatedId,
  561. ),
  562. endBinding: newBindingAfterDuplication(endBinding, oldIdToDuplicatedId),
  563. });
  564. });
  565. // Update the bindable shapes
  566. sceneElements
  567. .filter(({ id }) => allBindableElementIds.has(id))
  568. .forEach((bindableElement) => {
  569. const { boundElementIds } = bindableElement;
  570. if (boundElementIds != null && boundElementIds.length > 0) {
  571. mutateElement(bindableElement, {
  572. boundElementIds: boundElementIds.map(
  573. (boundElementId) =>
  574. oldIdToDuplicatedId.get(boundElementId) ?? boundElementId,
  575. ),
  576. });
  577. }
  578. });
  579. };
  580. const newBindingAfterDuplication = (
  581. binding: PointBinding | null,
  582. oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
  583. ): PointBinding | null => {
  584. if (binding == null) {
  585. return null;
  586. }
  587. const { elementId, focus, gap } = binding;
  588. return {
  589. focus,
  590. gap,
  591. elementId: oldIdToDuplicatedId.get(elementId) ?? elementId,
  592. };
  593. };
  594. export const fixBindingsAfterDeletion = (
  595. sceneElements: readonly ExcalidrawElement[],
  596. deletedElements: readonly ExcalidrawElement[],
  597. ): void => {
  598. const deletedElementIds = new Set(
  599. deletedElements.map((element) => element.id),
  600. );
  601. // Non deleted and need an update
  602. const boundElementIds: Set<ExcalidrawElement["id"]> = new Set();
  603. deletedElements.forEach((deletedElement) => {
  604. if (isBindableElement(deletedElement)) {
  605. deletedElement.boundElementIds?.forEach((id) => {
  606. if (!deletedElementIds.has(id)) {
  607. boundElementIds.add(id);
  608. }
  609. });
  610. }
  611. });
  612. (sceneElements.filter(({ id }) =>
  613. boundElementIds.has(id),
  614. ) as ExcalidrawLinearElement[]).forEach(
  615. (element: ExcalidrawLinearElement) => {
  616. const { startBinding, endBinding } = element;
  617. mutateElement(element, {
  618. startBinding: newBindingAfterDeletion(startBinding, deletedElementIds),
  619. endBinding: newBindingAfterDeletion(endBinding, deletedElementIds),
  620. });
  621. },
  622. );
  623. };
  624. const newBindingAfterDeletion = (
  625. binding: PointBinding | null,
  626. deletedElementIds: Set<ExcalidrawElement["id"]>,
  627. ): PointBinding | null => {
  628. if (binding == null || deletedElementIds.has(binding.elementId)) {
  629. return null;
  630. }
  631. return binding;
  632. };