feat: image support (#4011)
Co-authored-by: Emil Atanasov <heitara@gmail.com> Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
@ -23,6 +23,7 @@ import {
|
||||
ExcalidrawEllipseElement,
|
||||
NonDeleted,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
||||
@ -30,6 +31,7 @@ import { Point } from "../types";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { isImageElement } from "./typeChecks";
|
||||
|
||||
const isElementDraggableFromInside = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
@ -47,8 +49,7 @@ const isElementDraggableFromInside = (
|
||||
if (element.type === "line") {
|
||||
return isDraggableFromInside && isPathALoop(element.points);
|
||||
}
|
||||
|
||||
return isDraggableFromInside;
|
||||
return isDraggableFromInside || isImageElement(element);
|
||||
};
|
||||
|
||||
export const hitTest = (
|
||||
@ -161,6 +162,7 @@ type HitTestArgs = {
|
||||
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||
switch (args.element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "ellipse":
|
||||
@ -195,6 +197,7 @@ export const distanceToBindableElement = (
|
||||
): number => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
return distanceToRectangle(element, point);
|
||||
case "diamond":
|
||||
@ -224,7 +227,8 @@ const distanceToRectangle = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement,
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement,
|
||||
point: Point,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||
@ -486,6 +490,7 @@ export const determineFocusDistance = (
|
||||
const nabs = Math.abs(n);
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
return c / (hwidth * (nabs + q * mabs));
|
||||
case "diamond":
|
||||
@ -516,6 +521,7 @@ export const determineFocusPoint = (
|
||||
let point;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
@ -565,6 +571,7 @@ const getSortedElementLineIntersections = (
|
||||
let intersections: GA.Point[];
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
const corners = getCorners(element);
|
||||
@ -598,6 +605,7 @@ const getSortedElementLineIntersections = (
|
||||
const getCorners = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
scale: number = 1,
|
||||
@ -606,6 +614,7 @@ const getCorners = (
|
||||
const hy = (scale * element.height) / 2;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
@ -747,6 +756,7 @@ export const findFocusPointForEllipse = (
|
||||
export const findFocusPointForRectangulars = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
|
@ -62,25 +62,32 @@ export const dragNewElement = (
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
isResizeWithSidesSameLength: boolean,
|
||||
isResizeCenterPoint: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||
true */
|
||||
widthAspectRatio?: number | null,
|
||||
) => {
|
||||
if (isResizeWithSidesSameLength) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
elementType,
|
||||
width,
|
||||
y < originY ? -height : height,
|
||||
));
|
||||
if (shouldMaintainAspectRatio) {
|
||||
if (widthAspectRatio) {
|
||||
height = width / widthAspectRatio;
|
||||
} else {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
elementType,
|
||||
width,
|
||||
y < originY ? -height : height,
|
||||
));
|
||||
|
||||
if (height < 0) {
|
||||
height = -height;
|
||||
if (height < 0) {
|
||||
height = -height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newX = x < originX ? originX - width : originX;
|
||||
let newY = y < originY ? originY - height : originY;
|
||||
|
||||
if (isResizeCenterPoint) {
|
||||
if (shouldResizeFromCenter) {
|
||||
width += width;
|
||||
height += height;
|
||||
newX = originX - width / 2;
|
||||
|
111
src/element/image.ts
Normal file
111
src/element/image.ts
Normal file
@ -0,0 +1,111 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ExcalidrawImageElement & related helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import { MIME_TYPES, SVG_NS } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { AppClassProperties, DataURL, BinaryFiles } from "../types";
|
||||
import { isInitializedImageElement } from "./typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "./types";
|
||||
|
||||
export const loadHTMLImageElement = (dataURL: DataURL) => {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
image.src = dataURL;
|
||||
});
|
||||
};
|
||||
|
||||
/** NOTE: updates cache even if already populated with given image. Thus,
|
||||
* you should filter out the images upstream if you want to optimize this. */
|
||||
export const updateImageCache = async ({
|
||||
fileIds,
|
||||
files,
|
||||
imageCache,
|
||||
}: {
|
||||
fileIds: FileId[];
|
||||
files: BinaryFiles;
|
||||
imageCache: AppClassProperties["imageCache"];
|
||||
}) => {
|
||||
const updatedFiles = new Map<FileId, true>();
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
|
||||
await Promise.all(
|
||||
fileIds.reduce((promises, fileId) => {
|
||||
const fileData = files[fileId as string];
|
||||
if (fileData && !updatedFiles.has(fileId)) {
|
||||
updatedFiles.set(fileId, true);
|
||||
return promises.concat(
|
||||
(async () => {
|
||||
try {
|
||||
if (fileData.mimeType === MIME_TYPES.binary) {
|
||||
throw new Error("Only images can be added to ImageCache");
|
||||
}
|
||||
|
||||
const imagePromise = loadHTMLImageElement(fileData.dataURL);
|
||||
const data = {
|
||||
image: imagePromise,
|
||||
mimeType: fileData.mimeType,
|
||||
} as const;
|
||||
// store the promise immediately to indicate there's an in-progress
|
||||
// initialization
|
||||
imageCache.set(fileId, data);
|
||||
|
||||
const image = await imagePromise;
|
||||
|
||||
imageCache.set(fileId, { ...data, image });
|
||||
} catch (error) {
|
||||
erroredFiles.set(fileId, true);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
return promises;
|
||||
}, [] as Promise<any>[]),
|
||||
);
|
||||
|
||||
return {
|
||||
imageCache,
|
||||
/** includes errored files because they cache was updated nonetheless */
|
||||
updatedFiles,
|
||||
/** files that failed when creating HTMLImageElement */
|
||||
erroredFiles,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInitializedImageElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) =>
|
||||
elements.filter((element) =>
|
||||
isInitializedImageElement(element),
|
||||
) as InitializedExcalidrawImageElement[];
|
||||
|
||||
export const isHTMLSVGElement = (node: Node | null): node is SVGElement => {
|
||||
// lower-casing due to XML/HTML convention differences
|
||||
// https://johnresig.com/blog/nodename-case-sensitivity
|
||||
return node?.nodeName.toLowerCase() === "svg";
|
||||
};
|
||||
|
||||
export const normalizeSVG = async (SVGString: string) => {
|
||||
const doc = new DOMParser().parseFromString(SVGString, MIME_TYPES.svg);
|
||||
const svg = doc.querySelector("svg");
|
||||
const errorNode = doc.querySelector("parsererror");
|
||||
if (errorNode || !isHTMLSVGElement(svg)) {
|
||||
throw new Error(t("errors.invalidSVGString"));
|
||||
} else {
|
||||
if (!svg.hasAttribute("xmlns")) {
|
||||
svg.setAttribute("xmlns", SVG_NS);
|
||||
}
|
||||
|
||||
return svg.outerHTML;
|
||||
}
|
||||
};
|
@ -11,6 +11,7 @@ export {
|
||||
newTextElement,
|
||||
updateTextElement,
|
||||
newLinearElement,
|
||||
newImageElement,
|
||||
duplicateElement,
|
||||
} from "./newElement";
|
||||
export {
|
||||
@ -93,6 +94,10 @@ const _clearElements = (
|
||||
: element,
|
||||
);
|
||||
|
||||
export const clearElementsForDatabase = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => _clearElements(elements);
|
||||
|
||||
export const clearElementsForExport = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => _clearElements(elements);
|
||||
|
@ -17,12 +17,13 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
) => {
|
||||
informMutation = true,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points } = updates as any;
|
||||
const { points, fileId } = updates as any;
|
||||
|
||||
if (typeof points !== "undefined") {
|
||||
updates = { ...getSizeFromPoints(points), ...updates };
|
||||
@ -33,13 +34,23 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
if (typeof value !== "undefined") {
|
||||
if (
|
||||
(element as any)[key] === value &&
|
||||
// if object, always update in case its deep prop was mutated
|
||||
(typeof value !== "object" || value === null || key === "groupIds")
|
||||
// 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 === "points") {
|
||||
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) {
|
||||
@ -66,14 +77,14 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return;
|
||||
return element;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updates.height !== "undefined" ||
|
||||
typeof updates.width !== "undefined" ||
|
||||
typeof fileId != "undefined" ||
|
||||
typeof points !== "undefined"
|
||||
) {
|
||||
invalidateShapeForElement(element);
|
||||
@ -81,7 +92,12 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
Scene.getScene(element)?.informMutation();
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.informMutation();
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
@ -94,8 +110,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
if (typeof value !== "undefined") {
|
||||
if (
|
||||
(element as any)[key] === value &&
|
||||
// if object, always update in case its deep prop was mutated
|
||||
(typeof value !== "object" || value === null || key === "groupIds")
|
||||
// if object, always update because its attrs could have changed
|
||||
(typeof value !== "object" || value === null)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawGenericElement,
|
||||
@ -248,6 +249,22 @@ export const newLinearElement = (
|
||||
};
|
||||
};
|
||||
|
||||
export const newImageElement = (
|
||||
opts: {
|
||||
type: ExcalidrawImageElement["type"];
|
||||
} & 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: "pending",
|
||||
fileId: null,
|
||||
scale: [1, 1],
|
||||
};
|
||||
};
|
||||
|
||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||
//
|
||||
|
@ -47,9 +47,9 @@ export const transformElements = (
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
isResizeCenterPoint: boolean,
|
||||
shouldKeepSidesRatio: boolean,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
centerX: number,
|
||||
@ -62,7 +62,7 @@ export const transformElements = (
|
||||
element,
|
||||
pointerX,
|
||||
pointerY,
|
||||
isRotateWithDiscreteAngle,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
);
|
||||
updateBoundElements(element);
|
||||
} else if (
|
||||
@ -76,7 +76,7 @@ export const transformElements = (
|
||||
reshapeSingleTwoPointElement(
|
||||
element,
|
||||
resizeArrowDirection,
|
||||
isRotateWithDiscreteAngle,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
@ -90,7 +90,7 @@ export const transformElements = (
|
||||
resizeSingleTextElement(
|
||||
element,
|
||||
transformHandleType,
|
||||
isResizeCenterPoint,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
@ -98,10 +98,10 @@ export const transformElements = (
|
||||
} else if (transformHandleType) {
|
||||
resizeSingleElement(
|
||||
pointerDownState.originalElements.get(element.id) as typeof element,
|
||||
shouldKeepSidesRatio,
|
||||
shouldMaintainAspectRatio,
|
||||
element,
|
||||
transformHandleType,
|
||||
isResizeCenterPoint,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
@ -115,7 +115,7 @@ export const transformElements = (
|
||||
selectedElements,
|
||||
pointerX,
|
||||
pointerY,
|
||||
isRotateWithDiscreteAngle,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
centerX,
|
||||
centerY,
|
||||
);
|
||||
@ -142,13 +142,13 @@ const rotateSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
||||
if (isRotateWithDiscreteAngle) {
|
||||
if (shouldRotateWithDiscreteAngle) {
|
||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
@ -187,7 +187,7 @@ const getPerfectElementSizeWithRotation = (
|
||||
export const reshapeSingleTwoPointElement = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
@ -212,7 +212,7 @@ export const reshapeSingleTwoPointElement = (
|
||||
element.x + element.points[1][0] - rotatedX,
|
||||
element.y + element.points[1][1] - rotatedY,
|
||||
];
|
||||
if (isRotateWithDiscreteAngle) {
|
||||
if (shouldRotateWithDiscreteAngle) {
|
||||
[width, height] = getPerfectElementSizeWithRotation(
|
||||
element.type,
|
||||
width,
|
||||
@ -281,28 +281,28 @@ const measureFontSizeFromWH = (
|
||||
|
||||
const getSidesForTransformHandle = (
|
||||
transformHandleType: TransformHandleType,
|
||||
isResizeFromCenter: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
) => {
|
||||
return {
|
||||
n:
|
||||
/^(n|ne|nw)$/.test(transformHandleType) ||
|
||||
(isResizeFromCenter && /^(s|se|sw)$/.test(transformHandleType)),
|
||||
(shouldResizeFromCenter && /^(s|se|sw)$/.test(transformHandleType)),
|
||||
s:
|
||||
/^(s|se|sw)$/.test(transformHandleType) ||
|
||||
(isResizeFromCenter && /^(n|ne|nw)$/.test(transformHandleType)),
|
||||
(shouldResizeFromCenter && /^(n|ne|nw)$/.test(transformHandleType)),
|
||||
w:
|
||||
/^(w|nw|sw)$/.test(transformHandleType) ||
|
||||
(isResizeFromCenter && /^(e|ne|se)$/.test(transformHandleType)),
|
||||
(shouldResizeFromCenter && /^(e|ne|se)$/.test(transformHandleType)),
|
||||
e:
|
||||
/^(e|ne|se)$/.test(transformHandleType) ||
|
||||
(isResizeFromCenter && /^(w|nw|sw)$/.test(transformHandleType)),
|
||||
(shouldResizeFromCenter && /^(w|nw|sw)$/.test(transformHandleType)),
|
||||
};
|
||||
};
|
||||
|
||||
const resizeSingleTextElement = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
isResizeFromCenter: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
@ -361,7 +361,7 @@ const resizeSingleTextElement = (
|
||||
const deltaX2 = (x2 - nextX2) / 2;
|
||||
const deltaY2 = (y2 - nextY2) / 2;
|
||||
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
||||
getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
|
||||
getSidesForTransformHandle(transformHandleType, shouldResizeFromCenter),
|
||||
element.x,
|
||||
element.y,
|
||||
element.angle,
|
||||
@ -383,10 +383,10 @@ const resizeSingleTextElement = (
|
||||
|
||||
export const resizeSingleElement = (
|
||||
stateAtResizeStart: NonDeletedExcalidrawElement,
|
||||
shouldKeepSidesRatio: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
transformHandleDirection: TransformHandleDirection,
|
||||
isResizeFromCenter: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
@ -444,13 +444,13 @@ export const resizeSingleElement = (
|
||||
let eleNewHeight = element.height * scaleY;
|
||||
|
||||
// adjust dimensions for resizing from center
|
||||
if (isResizeFromCenter) {
|
||||
if (shouldResizeFromCenter) {
|
||||
eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
|
||||
eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
|
||||
}
|
||||
|
||||
// adjust dimensions to keep sides ratio
|
||||
if (shouldKeepSidesRatio) {
|
||||
if (shouldMaintainAspectRatio) {
|
||||
const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
|
||||
const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
|
||||
if (transformHandleDirection.length === 1) {
|
||||
@ -495,7 +495,7 @@ export const resizeSingleElement = (
|
||||
}
|
||||
|
||||
// Keeps opposite handle fixed during resize
|
||||
if (shouldKeepSidesRatio) {
|
||||
if (shouldMaintainAspectRatio) {
|
||||
if (["s", "n"].includes(transformHandleDirection)) {
|
||||
newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
|
||||
}
|
||||
@ -523,7 +523,7 @@ export const resizeSingleElement = (
|
||||
}
|
||||
}
|
||||
|
||||
if (isResizeFromCenter) {
|
||||
if (shouldResizeFromCenter) {
|
||||
newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
|
||||
newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
|
||||
}
|
||||
@ -558,6 +558,18 @@ export const resizeSingleElement = (
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
if ("scale" in element && "scale" in stateAtResizeStart) {
|
||||
mutateElement(element, {
|
||||
scale: [
|
||||
// defaulting because scaleX/Y can be 0/-0
|
||||
(Math.sign(scaleX) || stateAtResizeStart.scale[0]) *
|
||||
stateAtResizeStart.scale[0],
|
||||
(Math.sign(scaleY) || stateAtResizeStart.scale[1]) *
|
||||
stateAtResizeStart.scale[1],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
resizedElement.width !== 0 &&
|
||||
resizedElement.height !== 0 &&
|
||||
@ -692,13 +704,13 @@ const rotateMultipleElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
) => {
|
||||
let centerAngle =
|
||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||
if (isRotateWithDiscreteAngle) {
|
||||
if (shouldRotateWithDiscreteAngle) {
|
||||
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
||||
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
InitializedExcalidrawImageElement,
|
||||
ExcalidrawImageElement,
|
||||
} from "./types";
|
||||
|
||||
export const isGenericElement = (
|
||||
@ -19,6 +21,18 @@ export const isGenericElement = (
|
||||
);
|
||||
};
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is InitializedExcalidrawImageElement => {
|
||||
return !!element && element.type === "image" && !!element.fileId;
|
||||
};
|
||||
|
||||
export const isImageElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawImageElement => {
|
||||
return !!element && element.type === "image";
|
||||
};
|
||||
|
||||
export const isTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawTextElement => {
|
||||
|
@ -63,6 +63,21 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
||||
type: "ellipse";
|
||||
};
|
||||
|
||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "image";
|
||||
fileId: FileId | null;
|
||||
/** whether respective file is persisted */
|
||||
status: "pending" | "saved" | "error";
|
||||
/** X and Y scale factors <-1, 1>, used for image axis flipping */
|
||||
scale: [number, number];
|
||||
}>;
|
||||
|
||||
export type InitializedExcalidrawImageElement = MarkNonNullable<
|
||||
ExcalidrawImageElement,
|
||||
"fileId"
|
||||
>;
|
||||
|
||||
/**
|
||||
* These are elements that don't have any additional properties.
|
||||
*/
|
||||
@ -81,10 +96,11 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawGenericElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement
|
||||
| ExcalidrawFreeDrawElement;
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
isDeleted: false;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
export type NonDeletedExcalidrawElement = NonDeleted<ExcalidrawElement>;
|
||||
@ -104,7 +120,8 @@ export type ExcalidrawBindableElement =
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawTextElement;
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawImageElement;
|
||||
|
||||
export type PointBinding = {
|
||||
elementId: ExcalidrawBindableElement["id"];
|
||||
@ -133,3 +150,5 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||
simulatePressure: boolean;
|
||||
lastCommittedPoint: Point | null;
|
||||
}>;
|
||||
|
||||
export type FileId = string & { _brand: "FileId" };
|
||||
|
Reference in New Issue
Block a user