binding.ts 23 KB

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