2020-03-17 20:55:40 +01:00
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
|
|
|
ExcalidrawTextElement,
|
|
|
|
ExcalidrawLinearElement,
|
|
|
|
ExcalidrawGenericElement,
|
2020-04-08 09:49:52 -07:00
|
|
|
NonDeleted,
|
2020-04-08 21:00:27 +01:00
|
|
|
TextAlign,
|
2020-05-26 13:07:46 -07:00
|
|
|
GroupId,
|
2020-03-17 20:55:40 +01:00
|
|
|
} from "../element/types";
|
2020-01-21 00:16:22 +01:00
|
|
|
import { measureText } from "../utils";
|
2020-03-23 16:38:41 -07:00
|
|
|
import { randomInteger, randomId } from "../random";
|
2020-04-03 14:16:14 +02:00
|
|
|
import { newElementWith } from "./mutateElement";
|
2020-05-26 13:07:46 -07:00
|
|
|
import nanoid from "nanoid";
|
|
|
|
import { getNewGroupIdsForDuplication } from "../groups";
|
2020-01-21 00:16:22 +01:00
|
|
|
|
2020-03-17 20:55:40 +01:00
|
|
|
type ElementConstructorOpts = {
|
|
|
|
x: ExcalidrawGenericElement["x"];
|
|
|
|
y: ExcalidrawGenericElement["y"];
|
|
|
|
strokeColor: ExcalidrawGenericElement["strokeColor"];
|
|
|
|
backgroundColor: ExcalidrawGenericElement["backgroundColor"];
|
|
|
|
fillStyle: ExcalidrawGenericElement["fillStyle"];
|
|
|
|
strokeWidth: ExcalidrawGenericElement["strokeWidth"];
|
2020-05-14 17:04:33 +02:00
|
|
|
strokeStyle: ExcalidrawGenericElement["strokeStyle"];
|
2020-03-17 20:55:40 +01:00
|
|
|
roughness: ExcalidrawGenericElement["roughness"];
|
|
|
|
opacity: ExcalidrawGenericElement["opacity"];
|
|
|
|
width?: ExcalidrawGenericElement["width"];
|
|
|
|
height?: ExcalidrawGenericElement["height"];
|
2020-04-02 17:40:26 +09:00
|
|
|
angle?: ExcalidrawGenericElement["angle"];
|
2020-03-17 20:55:40 +01:00
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
const _newElementBase = <T extends ExcalidrawElement>(
|
2020-03-17 20:55:40 +01:00
|
|
|
type: T["type"],
|
|
|
|
{
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
strokeColor,
|
|
|
|
backgroundColor,
|
|
|
|
fillStyle,
|
|
|
|
strokeWidth,
|
2020-05-14 17:04:33 +02:00
|
|
|
strokeStyle,
|
2020-03-17 20:55:40 +01:00
|
|
|
roughness,
|
|
|
|
opacity,
|
|
|
|
width = 0,
|
|
|
|
height = 0,
|
2020-04-02 17:40:26 +09:00
|
|
|
angle = 0,
|
2020-03-17 20:55:40 +01:00
|
|
|
...rest
|
2020-05-24 21:17:25 +02:00
|
|
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
2020-05-20 16:21:37 +03:00
|
|
|
) => ({
|
|
|
|
id: rest.id || randomId(),
|
|
|
|
type,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
angle,
|
|
|
|
strokeColor,
|
|
|
|
backgroundColor,
|
|
|
|
fillStyle,
|
|
|
|
strokeWidth,
|
|
|
|
strokeStyle,
|
|
|
|
roughness,
|
|
|
|
opacity,
|
|
|
|
seed: rest.seed ?? randomInteger(),
|
|
|
|
version: rest.version || 1,
|
|
|
|
versionNonce: rest.versionNonce ?? 0,
|
|
|
|
isDeleted: false as false,
|
2020-05-26 13:07:46 -07:00
|
|
|
groupIds: [],
|
2020-05-20 16:21:37 +03:00
|
|
|
});
|
2020-03-17 20:55:40 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
|
|
|
type: ExcalidrawGenericElement["type"];
|
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawGenericElement> =>
|
|
|
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
2020-01-08 19:54:42 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newTextElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
|
|
|
text: string;
|
|
|
|
font: string;
|
2020-04-08 21:00:27 +01:00
|
|
|
textAlign: TextAlign;
|
2020-03-17 20:55:40 +01:00
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawTextElement> => {
|
2020-04-08 21:00:27 +01:00
|
|
|
const metrics = measureText(opts.text, opts.font);
|
2020-04-03 14:16:14 +02:00
|
|
|
const textElement = newElementWith(
|
|
|
|
{
|
|
|
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
2020-04-08 21:00:27 +01:00
|
|
|
text: opts.text,
|
|
|
|
font: opts.font,
|
|
|
|
textAlign: opts.textAlign,
|
2020-04-03 14:16:14 +02:00
|
|
|
// Center the text
|
|
|
|
x: opts.x - metrics.width / 2,
|
|
|
|
y: opts.y - metrics.height / 2,
|
|
|
|
width: metrics.width,
|
|
|
|
height: metrics.height,
|
|
|
|
baseline: metrics.baseline,
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
);
|
2020-01-21 00:16:22 +01:00
|
|
|
|
|
|
|
return textElement;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-21 00:16:22 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const newLinearElement = (
|
2020-03-17 20:55:40 +01:00
|
|
|
opts: {
|
2020-03-18 16:43:06 +01:00
|
|
|
type: ExcalidrawLinearElement["type"];
|
|
|
|
lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
|
2020-03-17 20:55:40 +01:00
|
|
|
} & ElementConstructorOpts,
|
2020-05-20 16:21:37 +03:00
|
|
|
): NonDeleted<ExcalidrawLinearElement> => {
|
2020-03-17 20:55:40 +01:00
|
|
|
return {
|
|
|
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
|
|
|
points: [],
|
2020-03-18 16:43:06 +01:00
|
|
|
lastCommittedPoint: opts.lastCommittedPoint || null,
|
2020-03-17 20:55:40 +01:00
|
|
|
};
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-17 20:55:40 +01:00
|
|
|
|
2020-02-19 22:28:11 +01:00
|
|
|
// 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
|
2020-05-23 12:07:11 -07:00
|
|
|
export const deepCopyElement = (val: any, depth: number = 0) => {
|
2020-02-19 22:28:11 +01:00
|
|
|
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))
|
|
|
|
: {};
|
2020-03-07 10:20:38 -05:00
|
|
|
for (const key in val) {
|
|
|
|
if (val.hasOwnProperty(key)) {
|
2020-02-19 22:28:11 +01:00
|
|
|
// don't copy top-level shape property, which we want to regenerate
|
2020-03-07 10:20:38 -05:00
|
|
|
if (depth === 0 && (key === "shape" || key === "canvas")) {
|
2020-02-19 22:28:11 +01:00
|
|
|
continue;
|
|
|
|
}
|
2020-05-23 12:07:11 -07:00
|
|
|
tmp[key] = deepCopyElement(val[key], depth + 1);
|
2020-02-19 22:28:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return tmp;
|
2020-02-09 23:57:14 +01:00
|
|
|
}
|
|
|
|
|
2020-02-19 22:28:11 +01:00
|
|
|
if (Array.isArray(val)) {
|
|
|
|
let k = val.length;
|
|
|
|
const arr = new Array(k);
|
|
|
|
while (k--) {
|
2020-05-23 12:07:11 -07:00
|
|
|
arr[k] = deepCopyElement(val[k], depth + 1);
|
2020-02-19 22:28:11 +01:00
|
|
|
}
|
|
|
|
return arr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return val;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-19 22:28:11 +01:00
|
|
|
|
2020-05-26 13:07:46 -07:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-05-20 16:21:37 +03:00
|
|
|
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
2020-05-26 13:07:46 -07:00
|
|
|
editingGroupId: GroupId | null,
|
|
|
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
2020-03-17 20:55:40 +01:00
|
|
|
element: TElement,
|
|
|
|
overrides?: Partial<TElement>,
|
2020-05-20 16:21:37 +03:00
|
|
|
): TElement => {
|
2020-05-23 12:07:11 -07:00
|
|
|
let copy: TElement = deepCopyElement(element);
|
2020-03-23 16:38:41 -07:00
|
|
|
copy.id = randomId();
|
|
|
|
copy.seed = randomInteger();
|
2020-05-26 13:07:46 -07:00
|
|
|
copy.groupIds = getNewGroupIdsForDuplication(
|
|
|
|
copy.groupIds,
|
|
|
|
editingGroupId,
|
|
|
|
(groupId) => {
|
|
|
|
if (!groupIdMapForOperation.has(groupId)) {
|
|
|
|
groupIdMapForOperation.set(groupId, nanoid());
|
|
|
|
}
|
|
|
|
return groupIdMapForOperation.get(groupId)!;
|
|
|
|
},
|
|
|
|
);
|
2020-03-17 20:55:40 +01:00
|
|
|
if (overrides) {
|
|
|
|
copy = Object.assign(copy, overrides);
|
|
|
|
}
|
2020-01-08 19:54:42 +01:00
|
|
|
return copy;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|