binding.ts 23 KB

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