aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/element/newElement.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/element/newElement.ts')
-rw-r--r--packages/excalidraw/element/newElement.ts793
1 files changed, 793 insertions, 0 deletions
diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts
new file mode 100644
index 0000000..7d1f149
--- /dev/null
+++ b/packages/excalidraw/element/newElement.ts
@@ -0,0 +1,793 @@
+import type {
+ ExcalidrawElement,
+ ExcalidrawImageElement,
+ ExcalidrawTextElement,
+ ExcalidrawLinearElement,
+ ExcalidrawGenericElement,
+ NonDeleted,
+ TextAlign,
+ GroupId,
+ VerticalAlign,
+ Arrowhead,
+ ExcalidrawFreeDrawElement,
+ FontFamilyValues,
+ ExcalidrawTextContainer,
+ ExcalidrawFrameElement,
+ ExcalidrawEmbeddableElement,
+ ExcalidrawMagicFrameElement,
+ ExcalidrawIframeElement,
+ ElementsMap,
+ ExcalidrawArrowElement,
+ FixedSegment,
+ ExcalidrawElbowArrowElement,
+} from "./types";
+import {
+ arrayToMap,
+ getFontString,
+ getUpdatedTimestamp,
+ isTestEnv,
+} from "../utils";
+import { randomInteger, randomId } from "../random";
+import { bumpVersion, newElementWith } from "./mutateElement";
+import { getNewGroupIdsForDuplication } from "../groups";
+import type { AppState } from "../types";
+import { getElementAbsoluteCoords } from ".";
+import { getResizedElementAbsoluteCoords } from "./bounds";
+import { getBoundTextMaxWidth } from "./textElement";
+import { wrapText } from "./textWrapping";
+import {
+ DEFAULT_ELEMENT_PROPS,
+ DEFAULT_FONT_FAMILY,
+ DEFAULT_FONT_SIZE,
+ DEFAULT_TEXT_ALIGN,
+ DEFAULT_VERTICAL_ALIGN,
+ ORIG_ID,
+ VERTICAL_ALIGN,
+} from "../constants";
+import type { MarkOptional, Merge, Mutable } from "../utility-types";
+import { getLineHeight } from "../fonts";
+import type { Radians } from "@excalidraw/math";
+import { normalizeText, measureText } from "./textMeasurements";
+
+export type ElementConstructorOpts = MarkOptional<
+ Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
+ | "width"
+ | "height"
+ | "angle"
+ | "groupIds"
+ | "frameId"
+ | "index"
+ | "boundElements"
+ | "seed"
+ | "version"
+ | "versionNonce"
+ | "link"
+ | "strokeStyle"
+ | "fillStyle"
+ | "strokeColor"
+ | "backgroundColor"
+ | "roughness"
+ | "strokeWidth"
+ | "roundness"
+ | "locked"
+ | "opacity"
+ | "customData"
+>;
+
+const _newElementBase = <T extends ExcalidrawElement>(
+ type: T["type"],
+ {
+ x,
+ y,
+ strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor,
+ backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor,
+ fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle,
+ strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth,
+ strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle,
+ roughness = DEFAULT_ELEMENT_PROPS.roughness,
+ opacity = DEFAULT_ELEMENT_PROPS.opacity,
+ width = 0,
+ height = 0,
+ angle = 0 as Radians,
+ groupIds = [],
+ frameId = null,
+ index = null,
+ roundness = null,
+ boundElements = null,
+ link = null,
+ locked = DEFAULT_ELEMENT_PROPS.locked,
+ ...rest
+ }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
+) => {
+ // NOTE (mtolmacs): This is a temporary check to detect extremely large
+ // element position or sizing
+ if (
+ x < -1e6 ||
+ x > 1e6 ||
+ y < -1e6 ||
+ y > 1e6 ||
+ width < -1e6 ||
+ width > 1e6 ||
+ height < -1e6 ||
+ height > 1e6
+ ) {
+ console.error("New element size or position is too large", {
+ x,
+ y,
+ width,
+ height,
+ // @ts-ignore
+ points: rest.points,
+ });
+ }
+
+ // assign type to guard against excess properties
+ const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
+ id: rest.id || randomId(),
+ type,
+ x,
+ y,
+ width,
+ height,
+ angle,
+ strokeColor,
+ backgroundColor,
+ fillStyle,
+ strokeWidth,
+ strokeStyle,
+ roughness,
+ opacity,
+ groupIds,
+ frameId,
+ index,
+ roundness,
+ seed: rest.seed ?? randomInteger(),
+ version: rest.version || 1,
+ versionNonce: rest.versionNonce ?? 0,
+ isDeleted: false as false,
+ boundElements,
+ updated: getUpdatedTimestamp(),
+ link,
+ locked,
+ customData: rest.customData,
+ };
+ return element;
+};
+
+export const newElement = (
+ opts: {
+ type: ExcalidrawGenericElement["type"];
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawGenericElement> =>
+ _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
+
+export const newEmbeddableElement = (
+ opts: {
+ type: "embeddable";
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawEmbeddableElement> => {
+ return _newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts);
+};
+
+export const newIframeElement = (
+ opts: {
+ type: "iframe";
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawIframeElement> => {
+ return {
+ ..._newElementBase<ExcalidrawIframeElement>("iframe", opts),
+ };
+};
+
+export const newFrameElement = (
+ opts: {
+ name?: string;
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawFrameElement> => {
+ const frameElement = newElementWith(
+ {
+ ..._newElementBase<ExcalidrawFrameElement>("frame", opts),
+ type: "frame",
+ name: opts?.name || null,
+ },
+ {},
+ );
+
+ return frameElement;
+};
+
+export const newMagicFrameElement = (
+ opts: {
+ name?: string;
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawMagicFrameElement> => {
+ const frameElement = newElementWith(
+ {
+ ..._newElementBase<ExcalidrawMagicFrameElement>("magicframe", opts),
+ type: "magicframe",
+ name: opts?.name || null,
+ },
+ {},
+ );
+
+ return frameElement;
+};
+
+/** computes element x/y offset based on textAlign/verticalAlign */
+const getTextElementPositionOffsets = (
+ opts: {
+ textAlign: ExcalidrawTextElement["textAlign"];
+ verticalAlign: ExcalidrawTextElement["verticalAlign"];
+ },
+ metrics: {
+ width: number;
+ height: number;
+ },
+) => {
+ return {
+ x:
+ opts.textAlign === "center"
+ ? metrics.width / 2
+ : opts.textAlign === "right"
+ ? metrics.width
+ : 0,
+ y: opts.verticalAlign === "middle" ? metrics.height / 2 : 0,
+ };
+};
+
+export const newTextElement = (
+ opts: {
+ text: string;
+ originalText?: string;
+ fontSize?: number;
+ fontFamily?: FontFamilyValues;
+ textAlign?: TextAlign;
+ verticalAlign?: VerticalAlign;
+ containerId?: ExcalidrawTextContainer["id"] | null;
+ lineHeight?: ExcalidrawTextElement["lineHeight"];
+ autoResize?: ExcalidrawTextElement["autoResize"];
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawTextElement> => {
+ const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
+ const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
+ const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
+ const text = normalizeText(opts.text);
+ const metrics = measureText(
+ text,
+ getFontString({ fontFamily, fontSize }),
+ lineHeight,
+ );
+ const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
+ const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
+ const offsets = getTextElementPositionOffsets(
+ { textAlign, verticalAlign },
+ metrics,
+ );
+
+ const textElementProps: ExcalidrawTextElement = {
+ ..._newElementBase<ExcalidrawTextElement>("text", opts),
+ text,
+ fontSize,
+ fontFamily,
+ textAlign,
+ verticalAlign,
+ x: opts.x - offsets.x,
+ y: opts.y - offsets.y,
+ width: metrics.width,
+ height: metrics.height,
+ containerId: opts.containerId || null,
+ originalText: opts.originalText ?? text,
+ autoResize: opts.autoResize ?? true,
+ lineHeight,
+ };
+
+ const textElement: ExcalidrawTextElement = newElementWith(
+ textElementProps,
+ {},
+ );
+
+ return textElement;
+};
+
+const getAdjustedDimensions = (
+ element: ExcalidrawTextElement,
+ elementsMap: ElementsMap,
+ nextText: string,
+): {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+} => {
+ let { width: nextWidth, height: nextHeight } = measureText(
+ nextText,
+ getFontString(element),
+ element.lineHeight,
+ );
+
+ // wrapped text
+ if (!element.autoResize) {
+ nextWidth = element.width;
+ }
+
+ const { textAlign, verticalAlign } = element;
+ let x: number;
+ let y: number;
+ if (
+ textAlign === "center" &&
+ verticalAlign === VERTICAL_ALIGN.MIDDLE &&
+ !element.containerId &&
+ element.autoResize
+ ) {
+ const prevMetrics = measureText(
+ element.text,
+ getFontString(element),
+ element.lineHeight,
+ );
+ const offsets = getTextElementPositionOffsets(element, {
+ width: nextWidth - prevMetrics.width,
+ height: nextHeight - prevMetrics.height,
+ });
+
+ x = element.x - offsets.x;
+ y = element.y - offsets.y;
+ } else {
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
+
+ const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
+ element,
+ nextWidth,
+ nextHeight,
+ false,
+ );
+ const deltaX1 = (x1 - nextX1) / 2;
+ const deltaY1 = (y1 - nextY1) / 2;
+ const deltaX2 = (x2 - nextX2) / 2;
+ const deltaY2 = (y2 - nextY2) / 2;
+
+ [x, y] = adjustXYWithRotation(
+ {
+ s: true,
+ e: textAlign === "center" || textAlign === "left",
+ w: textAlign === "center" || textAlign === "right",
+ },
+ element.x,
+ element.y,
+ element.angle,
+ deltaX1,
+ deltaY1,
+ deltaX2,
+ deltaY2,
+ );
+ }
+
+ return {
+ width: nextWidth,
+ height: nextHeight,
+ x: Number.isFinite(x) ? x : element.x,
+ y: Number.isFinite(y) ? y : element.y,
+ };
+};
+
+const adjustXYWithRotation = (
+ sides: {
+ n?: boolean;
+ e?: boolean;
+ s?: boolean;
+ w?: boolean;
+ },
+ x: number,
+ y: number,
+ angle: number,
+ deltaX1: number,
+ deltaY1: number,
+ deltaX2: number,
+ deltaY2: number,
+): [number, number] => {
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ if (sides.e && sides.w) {
+ x += deltaX1 + deltaX2;
+ } else if (sides.e) {
+ x += deltaX1 * (1 + cos);
+ y += deltaX1 * sin;
+ x += deltaX2 * (1 - cos);
+ y += deltaX2 * -sin;
+ } else if (sides.w) {
+ x += deltaX1 * (1 - cos);
+ y += deltaX1 * -sin;
+ x += deltaX2 * (1 + cos);
+ y += deltaX2 * sin;
+ }
+
+ if (sides.n && sides.s) {
+ y += deltaY1 + deltaY2;
+ } else if (sides.n) {
+ x += deltaY1 * sin;
+ y += deltaY1 * (1 - cos);
+ x += deltaY2 * -sin;
+ y += deltaY2 * (1 + cos);
+ } else if (sides.s) {
+ x += deltaY1 * -sin;
+ y += deltaY1 * (1 + cos);
+ x += deltaY2 * sin;
+ y += deltaY2 * (1 - cos);
+ }
+ return [x, y];
+};
+
+export const refreshTextDimensions = (
+ textElement: ExcalidrawTextElement,
+ container: ExcalidrawTextContainer | null,
+ elementsMap: ElementsMap,
+ text = textElement.text,
+) => {
+ if (textElement.isDeleted) {
+ return;
+ }
+ if (container || !textElement.autoResize) {
+ text = wrapText(
+ text,
+ getFontString(textElement),
+ container
+ ? getBoundTextMaxWidth(container, textElement)
+ : textElement.width,
+ );
+ }
+ const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
+ return { text, ...dimensions };
+};
+
+export const newFreeDrawElement = (
+ opts: {
+ type: "freedraw";
+ points?: ExcalidrawFreeDrawElement["points"];
+ simulatePressure: boolean;
+ pressures?: ExcalidrawFreeDrawElement["pressures"];
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawFreeDrawElement> => {
+ return {
+ ..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
+ points: opts.points || [],
+ pressures: opts.pressures || [],
+ simulatePressure: opts.simulatePressure,
+ lastCommittedPoint: null,
+ };
+};
+
+export const newLinearElement = (
+ opts: {
+ type: ExcalidrawLinearElement["type"];
+ points?: ExcalidrawLinearElement["points"];
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawLinearElement> => {
+ return {
+ ..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
+ points: opts.points || [],
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ };
+};
+
+export const newArrowElement = <T extends boolean>(
+ opts: {
+ type: ExcalidrawArrowElement["type"];
+ startArrowhead?: Arrowhead | null;
+ endArrowhead?: Arrowhead | null;
+ points?: ExcalidrawArrowElement["points"];
+ elbowed?: T;
+ fixedSegments?: FixedSegment[] | null;
+ } & ElementConstructorOpts,
+): T extends true
+ ? NonDeleted<ExcalidrawElbowArrowElement>
+ : NonDeleted<ExcalidrawArrowElement> => {
+ if (opts.elbowed) {
+ return {
+ ..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
+ points: opts.points || [],
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: opts.startArrowhead || null,
+ endArrowhead: opts.endArrowhead || null,
+ elbowed: true,
+ fixedSegments: opts.fixedSegments || [],
+ startIsSpecial: false,
+ endIsSpecial: false,
+ } as NonDeleted<ExcalidrawElbowArrowElement>;
+ }
+
+ return {
+ ..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
+ points: opts.points || [],
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: opts.startArrowhead || null,
+ endArrowhead: opts.endArrowhead || null,
+ elbowed: false,
+ } as T extends true
+ ? NonDeleted<ExcalidrawElbowArrowElement>
+ : NonDeleted<ExcalidrawArrowElement>;
+};
+
+export const newImageElement = (
+ opts: {
+ type: ExcalidrawImageElement["type"];
+ status?: ExcalidrawImageElement["status"];
+ fileId?: ExcalidrawImageElement["fileId"];
+ scale?: ExcalidrawImageElement["scale"];
+ crop?: ExcalidrawImageElement["crop"];
+ } & ElementConstructorOpts,
+): NonDeleted<ExcalidrawImageElement> => {
+ return {
+ ..._newElementBase<ExcalidrawImageElement>("image", opts),
+ // in the future we'll support changing stroke color for some SVG elements,
+ // and `transparent` will likely mean "use original colors of the image"
+ strokeColor: "transparent",
+ status: opts.status ?? "pending",
+ fileId: opts.fileId ?? null,
+ scale: opts.scale ?? [1, 1],
+ crop: opts.crop ?? null,
+ };
+};
+
+// Simplified deep clone for the purpose of cloning ExcalidrawElement.
+//
+// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
+// Typed arrays and other non-null objects.
+//
+// Adapted from https://github.com/lukeed/klona
+//
+// The reason for `deepCopyElement()` wrapper is type safety (only allow
+// passing ExcalidrawElement as the top-level argument).
+const _deepCopyElement = (val: any, depth: number = 0) => {
+ // only clone non-primitives
+ if (val == null || typeof val !== "object") {
+ return val;
+ }
+
+ const objectType = Object.prototype.toString.call(val);
+
+ if (objectType === "[object Object]") {
+ const tmp =
+ typeof val.constructor === "function"
+ ? Object.create(Object.getPrototypeOf(val))
+ : {};
+ for (const key in val) {
+ if (val.hasOwnProperty(key)) {
+ // don't copy non-serializable objects like these caches. They'll be
+ // populated when the element is rendered.
+ if (depth === 0 && (key === "shape" || key === "canvas")) {
+ continue;
+ }
+ tmp[key] = _deepCopyElement(val[key], depth + 1);
+ }
+ }
+ return tmp;
+ }
+
+ if (Array.isArray(val)) {
+ let k = val.length;
+ const arr = new Array(k);
+ while (k--) {
+ arr[k] = _deepCopyElement(val[k], depth + 1);
+ }
+ return arr;
+ }
+
+ // we're not cloning non-array & non-plain-object objects because we
+ // don't support them on excalidraw elements yet. If we do, we need to make
+ // sure we start cloning them, so let's warn about it.
+ if (import.meta.env.DEV) {
+ if (
+ objectType !== "[object Object]" &&
+ objectType !== "[object Array]" &&
+ objectType.startsWith("[object ")
+ ) {
+ console.warn(
+ `_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
+ );
+ }
+ }
+
+ return val;
+};
+
+/**
+ * Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
+ * any value. The purpose is to to break object references for immutability
+ * reasons, whenever we want to keep the original element, but ensure it's not
+ * mutated.
+ *
+ * Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
+ * Typed arrays and other non-null objects.
+ */
+export const deepCopyElement = <T extends ExcalidrawElement>(
+ val: T,
+): Mutable<T> => {
+ return _deepCopyElement(val);
+};
+
+const __test__defineOrigId = (clonedObj: object, origId: string) => {
+ Object.defineProperty(clonedObj, ORIG_ID, {
+ value: origId,
+ writable: false,
+ enumerable: false,
+ });
+};
+
+/**
+ * utility wrapper to generate new id.
+ */
+const regenerateId = () => {
+ return randomId();
+};
+
+/**
+ * Duplicate an element, often used in the alt-drag operation.
+ * Note that this method has gotten a bit complicated since the
+ * introduction of gruoping/ungrouping elements.
+ * @param editingGroupId The current group being edited. The new
+ * element will inherit this group and its
+ * parents.
+ * @param groupIdMapForOperation A Map that maps old group IDs to
+ * duplicated ones. If you are duplicating
+ * multiple elements at once, share this map
+ * amongst all of them
+ * @param element Element to duplicate
+ * @param overrides Any element properties to override
+ */
+export const duplicateElement = <TElement extends ExcalidrawElement>(
+ editingGroupId: AppState["editingGroupId"],
+ groupIdMapForOperation: Map<GroupId, GroupId>,
+ element: TElement,
+ overrides?: Partial<TElement>,
+): Readonly<TElement> => {
+ let copy = deepCopyElement(element);
+
+ if (isTestEnv()) {
+ __test__defineOrigId(copy, element.id);
+ }
+
+ copy.id = regenerateId();
+ copy.boundElements = null;
+ copy.updated = getUpdatedTimestamp();
+ copy.seed = randomInteger();
+ copy.groupIds = getNewGroupIdsForDuplication(
+ copy.groupIds,
+ editingGroupId,
+ (groupId) => {
+ if (!groupIdMapForOperation.has(groupId)) {
+ groupIdMapForOperation.set(groupId, regenerateId());
+ }
+ return groupIdMapForOperation.get(groupId)!;
+ },
+ );
+ if (overrides) {
+ copy = Object.assign(copy, overrides);
+ }
+ return copy;
+};
+
+/**
+ * Clones elements, regenerating their ids (including bindings) and group ids.
+ *
+ * If bindings don't exist in the elements array, they are removed. Therefore,
+ * it's advised to supply the whole elements array, or sets of elements that
+ * are encapsulated (such as library items), if the purpose is to retain
+ * bindings to the cloned elements intact.
+ *
+ * NOTE by default does not randomize or regenerate anything except the id.
+ */
+export const duplicateElements = (
+ elements: readonly ExcalidrawElement[],
+ opts?: {
+ /** NOTE also updates version flags and `updated` */
+ randomizeSeed: boolean;
+ },
+) => {
+ const clonedElements: ExcalidrawElement[] = [];
+
+ const origElementsMap = arrayToMap(elements);
+
+ // used for for migrating old ids to new ids
+ const elementNewIdsMap = new Map<
+ /* orig */ ExcalidrawElement["id"],
+ /* new */ ExcalidrawElement["id"]
+ >();
+
+ const maybeGetNewId = (id: ExcalidrawElement["id"]) => {
+ // if we've already migrated the element id, return the new one directly
+ if (elementNewIdsMap.has(id)) {
+ return elementNewIdsMap.get(id)!;
+ }
+ // if we haven't migrated the element id, but an old element with the same
+ // id exists, generate a new id for it and return it
+ if (origElementsMap.has(id)) {
+ const newId = regenerateId();
+ elementNewIdsMap.set(id, newId);
+ return newId;
+ }
+ // if old element doesn't exist, return null to mark it for removal
+ return null;
+ };
+
+ const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
+
+ for (const element of elements) {
+ const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
+
+ clonedElement.id = maybeGetNewId(element.id)!;
+ if (isTestEnv()) {
+ __test__defineOrigId(clonedElement, element.id);
+ }
+
+ if (opts?.randomizeSeed) {
+ clonedElement.seed = randomInteger();
+ bumpVersion(clonedElement);
+ }
+
+ if (clonedElement.groupIds) {
+ clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
+ if (!groupNewIdsMap.has(groupId)) {
+ groupNewIdsMap.set(groupId, regenerateId());
+ }
+ return groupNewIdsMap.get(groupId)!;
+ });
+ }
+
+ if ("containerId" in clonedElement && clonedElement.containerId) {
+ const newContainerId = maybeGetNewId(clonedElement.containerId);
+ clonedElement.containerId = newContainerId;
+ }
+
+ if ("boundElements" in clonedElement && clonedElement.boundElements) {
+ clonedElement.boundElements = clonedElement.boundElements.reduce(
+ (
+ acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
+ binding,
+ ) => {
+ const newBindingId = maybeGetNewId(binding.id);
+ if (newBindingId) {
+ acc.push({ ...binding, id: newBindingId });
+ }
+ return acc;
+ },
+ [],
+ );
+ }
+
+ if ("endBinding" in clonedElement && clonedElement.endBinding) {
+ const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
+ clonedElement.endBinding = newEndBindingId
+ ? {
+ ...clonedElement.endBinding,
+ elementId: newEndBindingId,
+ }
+ : null;
+ }
+ if ("startBinding" in clonedElement && clonedElement.startBinding) {
+ const newEndBindingId = maybeGetNewId(
+ clonedElement.startBinding.elementId,
+ );
+ clonedElement.startBinding = newEndBindingId
+ ? {
+ ...clonedElement.startBinding,
+ elementId: newEndBindingId,
+ }
+ : null;
+ }
+
+ if (clonedElement.frameId) {
+ clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
+ }
+
+ clonedElements.push(clonedElement);
+ }
+
+ return clonedElements;
+};