diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/element/mutateElement.ts | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/element/mutateElement.ts')
| -rw-r--r-- | packages/excalidraw/element/mutateElement.ts | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/packages/excalidraw/element/mutateElement.ts b/packages/excalidraw/element/mutateElement.ts new file mode 100644 index 0000000..cfff5c8 --- /dev/null +++ b/packages/excalidraw/element/mutateElement.ts @@ -0,0 +1,196 @@ +import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types"; +import Scene from "../scene/Scene"; +import { getSizeFromPoints } from "../points"; +import { randomInteger } from "../random"; +import { getUpdatedTimestamp, toBrandedType } from "../utils"; +import type { Mutable } from "../utility-types"; +import { ShapeCache } from "../scene/ShapeCache"; +import { isElbowArrow } from "./typeChecks"; +import { updateElbowArrowPoints } from "./elbowArrow"; +import type { Radians } from "@excalidraw/math"; + +export type ElementUpdate<TElement extends ExcalidrawElement> = Omit< + Partial<TElement>, + "id" | "version" | "versionNonce" | "updated" +>; + +// This function tracks updates of text elements for the purposes for collaboration. +// The version is used to compare updates when more than one user is working in +// the same drawing. Note: this will trigger the component to update. Make sure you +// are calling it either from a React event handler or within unstable_batchedUpdates(). +export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>( + element: TElement, + updates: ElementUpdate<TElement>, + informMutation = true, + options?: { + // Currently only for elbow arrows. + // If true, the elbow arrow tries to bind to the nearest element. If false + // it tries to keep the same bound element, if any. + isDragging?: boolean; + }, +): TElement => { + let didChange = false; + + // casting to any because can't use `in` operator + // (see https://github.com/microsoft/TypeScript/issues/21732) + const { points, fixedSegments, fileId, startBinding, endBinding } = + updates as any; + + if ( + isElbowArrow(element) && + (Object.keys(updates).length === 0 || // normalization case + typeof points !== "undefined" || // repositioning + typeof fixedSegments !== "undefined" || // segment fixing + typeof startBinding !== "undefined" || + typeof endBinding !== "undefined") // manual binding to element + ) { + const elementsMap = toBrandedType<NonDeletedSceneElementsMap>( + Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(), + ); + + updates = { + ...updates, + angle: 0 as Radians, + ...updateElbowArrowPoints( + { + ...element, + x: updates.x || element.x, + y: updates.y || element.y, + }, + elementsMap, + { + fixedSegments, + points, + startBinding, + endBinding, + }, + { + isDragging: options?.isDragging, + }, + ), + }; + } else if (typeof points !== "undefined") { + updates = { ...getSizeFromPoints(points), ...updates }; + } + + for (const key in updates) { + const value = (updates as any)[key]; + if (typeof value !== "undefined") { + if ( + (element as any)[key] === value && + // if object, always update because its attrs could have changed + // (except for specific keys we handle below) + (typeof value !== "object" || + value === null || + key === "groupIds" || + key === "scale") + ) { + continue; + } + + if (key === "scale") { + const prevScale = (element as any)[key]; + const nextScale = value; + if (prevScale[0] === nextScale[0] && prevScale[1] === nextScale[1]) { + continue; + } + } else if (key === "points") { + const prevPoints = (element as any)[key]; + const nextPoints = value; + if (prevPoints.length === nextPoints.length) { + let didChangePoints = false; + let index = prevPoints.length; + while (--index) { + const prevPoint = prevPoints[index]; + const nextPoint = nextPoints[index]; + if ( + prevPoint[0] !== nextPoint[0] || + prevPoint[1] !== nextPoint[1] + ) { + didChangePoints = true; + break; + } + } + if (!didChangePoints) { + continue; + } + } + } + + (element as any)[key] = value; + didChange = true; + } + } + + if (!didChange) { + return element; + } + + if ( + typeof updates.height !== "undefined" || + typeof updates.width !== "undefined" || + typeof fileId != "undefined" || + typeof points !== "undefined" + ) { + ShapeCache.delete(element); + } + + element.version++; + element.versionNonce = randomInteger(); + element.updated = getUpdatedTimestamp(); + + if (informMutation) { + Scene.getScene(element)?.triggerUpdate(); + } + + return element; +}; + +export const newElementWith = <TElement extends ExcalidrawElement>( + element: TElement, + updates: ElementUpdate<TElement>, + /** pass `true` to always regenerate */ + force = false, +): TElement => { + let didChange = false; + for (const key in updates) { + const value = (updates as any)[key]; + if (typeof value !== "undefined") { + if ( + (element as any)[key] === value && + // if object, always update because its attrs could have changed + (typeof value !== "object" || value === null) + ) { + continue; + } + didChange = true; + } + } + + if (!didChange && !force) { + return element; + } + + return { + ...element, + ...updates, + updated: getUpdatedTimestamp(), + version: element.version + 1, + versionNonce: randomInteger(), + }; +}; + +/** + * Mutates element, bumping `version`, `versionNonce`, and `updated`. + * + * NOTE: does not trigger re-render. + */ +export const bumpVersion = <T extends Mutable<ExcalidrawElement>>( + element: T, + version?: ExcalidrawElement["version"], +) => { + element.version = (version ?? element.version) + 1; + element.versionNonce = randomInteger(); + element.updated = getUpdatedTimestamp(); + return element; +}; |
