Scene.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import {
  2. ExcalidrawElement,
  3. NonDeletedExcalidrawElement,
  4. NonDeleted,
  5. } from "../element/types";
  6. import { getNonDeletedElements, isNonDeletedElement } from "../element";
  7. import { LinearElementEditor } from "../element/linearElementEditor";
  8. import App from "../components/App";
  9. import { isCustomElement } from "../element/typeChecks";
  10. type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
  11. type ElementKey = ExcalidrawElement | ElementIdKey;
  12. type SceneStateCallback = () => void;
  13. type SceneStateCallbackRemover = () => void;
  14. const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
  15. if (typeof elementKey === "string") {
  16. return true;
  17. }
  18. return false;
  19. };
  20. class Scene {
  21. // ---------------------------------------------------------------------------
  22. // static methods/props
  23. // ---------------------------------------------------------------------------
  24. private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
  25. private static sceneMapById = new Map<string, Scene>();
  26. private app: App;
  27. constructor(app: App) {
  28. this.app = app;
  29. }
  30. static mapElementToScene(elementKey: ElementKey, scene: Scene) {
  31. if (isIdKey(elementKey)) {
  32. this.sceneMapById.set(elementKey, scene);
  33. } else {
  34. this.sceneMapByElement.set(elementKey, scene);
  35. }
  36. }
  37. static getScene(elementKey: ElementKey): Scene | null {
  38. if (isIdKey(elementKey)) {
  39. return this.sceneMapById.get(elementKey) || null;
  40. }
  41. return this.sceneMapByElement.get(elementKey) || null;
  42. }
  43. // ---------------------------------------------------------------------------
  44. // instance methods/props
  45. // ---------------------------------------------------------------------------
  46. private callbacks: Set<SceneStateCallback> = new Set();
  47. private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
  48. private elements: readonly ExcalidrawElement[] = [];
  49. private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
  50. // TODO: getAllElementsIncludingDeleted
  51. getElementsIncludingDeleted() {
  52. return this.elements;
  53. }
  54. // TODO: getAllNonDeletedElements
  55. getElements(): readonly NonDeletedExcalidrawElement[] {
  56. return this.nonDeletedElements;
  57. }
  58. getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
  59. return (this.elementsMap.get(id) as T | undefined) || null;
  60. }
  61. getNonDeletedElement(
  62. id: ExcalidrawElement["id"],
  63. ): NonDeleted<ExcalidrawElement> | null {
  64. const element = this.getElement(id);
  65. if (element && isNonDeletedElement(element)) {
  66. return element;
  67. }
  68. return null;
  69. }
  70. // TODO: Rename methods here, this is confusing
  71. getNonDeletedElements(
  72. ids: readonly ExcalidrawElement["id"][],
  73. ): NonDeleted<ExcalidrawElement>[] {
  74. const result: NonDeleted<ExcalidrawElement>[] = [];
  75. ids.forEach((id) => {
  76. const element = this.getNonDeletedElement(id);
  77. if (element != null) {
  78. result.push(element);
  79. }
  80. });
  81. return result;
  82. }
  83. replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
  84. this.elements = [];
  85. const elements: ExcalidrawElement[] = [];
  86. this.elementsMap.clear();
  87. const elementsToBeStackedOnTop: ExcalidrawElement[] = [];
  88. nextElements.forEach((element) => {
  89. if (isCustomElement(element)) {
  90. const config =
  91. this.app.props.customElementsConfig?.[element.customType];
  92. if (config?.stackedOnTop) {
  93. elementsToBeStackedOnTop.push(element);
  94. } else {
  95. elements.push(element);
  96. }
  97. } else {
  98. elements.push(element);
  99. }
  100. this.elementsMap.set(element.id, element);
  101. Scene.mapElementToScene(element, this);
  102. });
  103. elementsToBeStackedOnTop.forEach((ele) => elements.push(ele));
  104. this.elements = elements;
  105. this.nonDeletedElements = getNonDeletedElements(this.elements);
  106. this.informMutation();
  107. }
  108. informMutation() {
  109. for (const callback of Array.from(this.callbacks)) {
  110. callback();
  111. }
  112. }
  113. addCallback(cb: SceneStateCallback): SceneStateCallbackRemover {
  114. if (this.callbacks.has(cb)) {
  115. throw new Error();
  116. }
  117. this.callbacks.add(cb);
  118. return () => {
  119. if (!this.callbacks.has(cb)) {
  120. throw new Error();
  121. }
  122. this.callbacks.delete(cb);
  123. };
  124. }
  125. destroy() {
  126. Scene.sceneMapById.forEach((scene, elementKey) => {
  127. if (scene === this) {
  128. Scene.sceneMapById.delete(elementKey);
  129. }
  130. });
  131. // done not for memory leaks, but to guard against possible late fires
  132. // (I guess?)
  133. this.callbacks.clear();
  134. }
  135. }
  136. export default Scene;