123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- import {
- ExcalidrawElement,
- ExcalidrawTextElement,
- ExcalidrawLinearElement,
- ExcalidrawGenericElement,
- NonDeleted,
- TextAlign,
- FontFamily,
- GroupId,
- VerticalAlign,
- } from "../element/types";
- import { measureText, getFontString } from "../utils";
- import { randomInteger, randomId } from "../random";
- import { newElementWith } from "./mutateElement";
- import { getNewGroupIdsForDuplication } from "../groups";
- import { AppState } from "../types";
- import { getElementAbsoluteCoords } from ".";
- import { adjustXYWithRotation } from "../math";
- import { getResizedElementAbsoluteCoords } from "./bounds";
- type ElementConstructorOpts = MarkOptional<
- Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted">,
- | "width"
- | "height"
- | "angle"
- | "groupIds"
- | "boundElementIds"
- | "seed"
- | "version"
- | "versionNonce"
- >;
- const _newElementBase = <T extends ExcalidrawElement>(
- type: T["type"],
- {
- x,
- y,
- strokeColor,
- backgroundColor,
- fillStyle,
- strokeWidth,
- strokeStyle,
- roughness,
- opacity,
- width = 0,
- height = 0,
- angle = 0,
- groupIds = [],
- strokeSharpness,
- boundElementIds = null,
- ...rest
- }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
- ) => ({
- id: rest.id || randomId(),
- type,
- x,
- y,
- width,
- height,
- angle,
- strokeColor,
- backgroundColor,
- fillStyle,
- strokeWidth,
- strokeStyle,
- roughness,
- opacity,
- groupIds,
- strokeSharpness,
- seed: rest.seed ?? randomInteger(),
- version: rest.version || 1,
- versionNonce: rest.versionNonce ?? 0,
- isDeleted: false as false,
- boundElementIds,
- });
- export const newElement = (
- opts: {
- type: ExcalidrawGenericElement["type"];
- } & ElementConstructorOpts,
- ): NonDeleted<ExcalidrawGenericElement> =>
- _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
- /** computes element x/y offset based on textAlign/verticalAlign */
- function 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;
- fontSize: number;
- fontFamily: FontFamily;
- textAlign: TextAlign;
- verticalAlign: VerticalAlign;
- } & ElementConstructorOpts,
- ): NonDeleted<ExcalidrawTextElement> => {
- const metrics = measureText(opts.text, getFontString(opts));
- const offsets = getTextElementPositionOffsets(opts, metrics);
- const textElement = newElementWith(
- {
- ..._newElementBase<ExcalidrawTextElement>("text", opts),
- text: opts.text,
- fontSize: opts.fontSize,
- fontFamily: opts.fontFamily,
- textAlign: opts.textAlign,
- verticalAlign: opts.verticalAlign,
- x: opts.x - offsets.x,
- y: opts.y - offsets.y,
- width: metrics.width,
- height: metrics.height,
- baseline: metrics.baseline,
- },
- {},
- );
- return textElement;
- };
- const getAdjustedDimensions = (
- element: ExcalidrawTextElement,
- nextText: string,
- ): {
- x: number;
- y: number;
- width: number;
- height: number;
- baseline: number;
- } => {
- const {
- width: nextWidth,
- height: nextHeight,
- baseline: nextBaseline,
- } = measureText(nextText, getFontString(element));
- const { textAlign, verticalAlign } = element;
- let x, y;
- if (textAlign === "center" && verticalAlign === "middle") {
- const prevMetrics = measureText(element.text, getFontString(element));
- 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);
- const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
- element,
- nextWidth,
- nextHeight,
- );
- 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,
- baseline: nextBaseline,
- };
- };
- export const updateTextElement = (
- element: ExcalidrawTextElement,
- { text, isDeleted }: { text: string; isDeleted?: boolean },
- ): ExcalidrawTextElement => {
- return newElementWith(element, {
- text,
- isDeleted: isDeleted ?? element.isDeleted,
- ...getAdjustedDimensions(element, text),
- });
- };
- export const newLinearElement = (
- opts: {
- type: ExcalidrawLinearElement["type"];
- } & ElementConstructorOpts,
- ): NonDeleted<ExcalidrawLinearElement> => {
- return {
- ..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
- points: [],
- lastCommittedPoint: null,
- startBinding: null,
- endBinding: null,
- };
- };
- // Simplified deep clone for the purpose of cloning ExcalidrawElement only
- // (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
- //
- // Adapted from https://github.com/lukeed/klona
- export const deepCopyElement = (val: any, depth: number = 0) => {
- if (val == null || typeof val !== "object") {
- return val;
- }
- if (Object.prototype.toString.call(val) === "[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 top-level shape property, which we want to regenerate
- 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;
- }
- return val;
- };
- /**
- * 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 Mutable<ExcalidrawElement>>(
- editingGroupId: AppState["editingGroupId"],
- groupIdMapForOperation: Map<GroupId, GroupId>,
- element: TElement,
- overrides?: Partial<TElement>,
- ): TElement => {
- let copy: TElement = deepCopyElement(element);
- copy.id = process.env.NODE_ENV === "test" ? `${copy.id}_copy` : randomId();
- copy.seed = randomInteger();
- copy.groupIds = getNewGroupIdsForDuplication(
- copy.groupIds,
- editingGroupId,
- (groupId) => {
- if (!groupIdMapForOperation.has(groupId)) {
- groupIdMapForOperation.set(groupId, randomId());
- }
- return groupIdMapForOperation.get(groupId)!;
- },
- );
- if (overrides) {
- copy = Object.assign(copy, overrides);
- }
- return copy;
- };
|