newElement.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {
  2. ExcalidrawElement,
  3. ExcalidrawTextElement,
  4. ExcalidrawLinearElement,
  5. ExcalidrawGenericElement,
  6. NonDeleted,
  7. TextAlign,
  8. FontFamily,
  9. GroupId,
  10. } from "../element/types";
  11. import { measureText, getFontString } from "../utils";
  12. import { randomInteger, randomId } from "../random";
  13. import { newElementWith } from "./mutateElement";
  14. import { getNewGroupIdsForDuplication } from "../groups";
  15. type ElementConstructorOpts = {
  16. x: ExcalidrawGenericElement["x"];
  17. y: ExcalidrawGenericElement["y"];
  18. strokeColor: ExcalidrawGenericElement["strokeColor"];
  19. backgroundColor: ExcalidrawGenericElement["backgroundColor"];
  20. fillStyle: ExcalidrawGenericElement["fillStyle"];
  21. strokeWidth: ExcalidrawGenericElement["strokeWidth"];
  22. strokeStyle: ExcalidrawGenericElement["strokeStyle"];
  23. roughness: ExcalidrawGenericElement["roughness"];
  24. opacity: ExcalidrawGenericElement["opacity"];
  25. width?: ExcalidrawGenericElement["width"];
  26. height?: ExcalidrawGenericElement["height"];
  27. angle?: ExcalidrawGenericElement["angle"];
  28. };
  29. const _newElementBase = <T extends ExcalidrawElement>(
  30. type: T["type"],
  31. {
  32. x,
  33. y,
  34. strokeColor,
  35. backgroundColor,
  36. fillStyle,
  37. strokeWidth,
  38. strokeStyle,
  39. roughness,
  40. opacity,
  41. width = 0,
  42. height = 0,
  43. angle = 0,
  44. ...rest
  45. }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
  46. ) => ({
  47. id: rest.id || randomId(),
  48. type,
  49. x,
  50. y,
  51. width,
  52. height,
  53. angle,
  54. strokeColor,
  55. backgroundColor,
  56. fillStyle,
  57. strokeWidth,
  58. strokeStyle,
  59. roughness,
  60. opacity,
  61. seed: rest.seed ?? randomInteger(),
  62. version: rest.version || 1,
  63. versionNonce: rest.versionNonce ?? 0,
  64. isDeleted: false as false,
  65. groupIds: [],
  66. });
  67. export const newElement = (
  68. opts: {
  69. type: ExcalidrawGenericElement["type"];
  70. } & ElementConstructorOpts,
  71. ): NonDeleted<ExcalidrawGenericElement> =>
  72. _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
  73. export const newTextElement = (
  74. opts: {
  75. text: string;
  76. fontSize: number;
  77. fontFamily: FontFamily;
  78. textAlign: TextAlign;
  79. } & ElementConstructorOpts,
  80. ): NonDeleted<ExcalidrawTextElement> => {
  81. const metrics = measureText(opts.text, getFontString(opts));
  82. const textElement = newElementWith(
  83. {
  84. ..._newElementBase<ExcalidrawTextElement>("text", opts),
  85. text: opts.text,
  86. fontSize: opts.fontSize,
  87. fontFamily: opts.fontFamily,
  88. textAlign: opts.textAlign,
  89. // Center the text
  90. x: opts.x - metrics.width / 2,
  91. y: opts.y - metrics.height / 2,
  92. width: metrics.width,
  93. height: metrics.height,
  94. baseline: metrics.baseline,
  95. },
  96. {},
  97. );
  98. return textElement;
  99. };
  100. export const newLinearElement = (
  101. opts: {
  102. type: ExcalidrawLinearElement["type"];
  103. lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
  104. } & ElementConstructorOpts,
  105. ): NonDeleted<ExcalidrawLinearElement> => {
  106. return {
  107. ..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
  108. points: [],
  109. lastCommittedPoint: opts.lastCommittedPoint || null,
  110. };
  111. };
  112. // Simplified deep clone for the purpose of cloning ExcalidrawElement only
  113. // (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
  114. //
  115. // Adapted from https://github.com/lukeed/klona
  116. export const deepCopyElement = (val: any, depth: number = 0) => {
  117. if (val == null || typeof val !== "object") {
  118. return val;
  119. }
  120. if (Object.prototype.toString.call(val) === "[object Object]") {
  121. const tmp =
  122. typeof val.constructor === "function"
  123. ? Object.create(Object.getPrototypeOf(val))
  124. : {};
  125. for (const key in val) {
  126. if (val.hasOwnProperty(key)) {
  127. // don't copy top-level shape property, which we want to regenerate
  128. if (depth === 0 && (key === "shape" || key === "canvas")) {
  129. continue;
  130. }
  131. tmp[key] = deepCopyElement(val[key], depth + 1);
  132. }
  133. }
  134. return tmp;
  135. }
  136. if (Array.isArray(val)) {
  137. let k = val.length;
  138. const arr = new Array(k);
  139. while (k--) {
  140. arr[k] = deepCopyElement(val[k], depth + 1);
  141. }
  142. return arr;
  143. }
  144. return val;
  145. };
  146. /**
  147. * Duplicate an element, often used in the alt-drag operation.
  148. * Note that this method has gotten a bit complicated since the
  149. * introduction of gruoping/ungrouping elements.
  150. * @param editingGroupId The current group being edited. The new
  151. * element will inherit this group and its
  152. * parents.
  153. * @param groupIdMapForOperation A Map that maps old group IDs to
  154. * duplicated ones. If you are duplicating
  155. * multiple elements at once, share this map
  156. * amongst all of them
  157. * @param element Element to duplicate
  158. * @param overrides Any element properties to override
  159. */
  160. export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
  161. editingGroupId: GroupId | null,
  162. groupIdMapForOperation: Map<GroupId, GroupId>,
  163. element: TElement,
  164. overrides?: Partial<TElement>,
  165. ): TElement => {
  166. let copy: TElement = deepCopyElement(element);
  167. copy.id = randomId();
  168. copy.seed = randomInteger();
  169. copy.groupIds = getNewGroupIdsForDuplication(
  170. copy.groupIds,
  171. editingGroupId,
  172. (groupId) => {
  173. if (!groupIdMapForOperation.has(groupId)) {
  174. groupIdMapForOperation.set(groupId, randomId());
  175. }
  176. return groupIdMapForOperation.get(groupId)!;
  177. },
  178. );
  179. if (overrides) {
  180. copy = Object.assign(copy, overrides);
  181. }
  182. return copy;
  183. };