Compare commits

..

1 Commits

Author SHA1 Message Date
dwelle
3c83a322b6 feat: support image background editor [wip] 2021-12-20 21:50:16 +01:00
53 changed files with 1221 additions and 4548 deletions

View File

@@ -21,12 +21,12 @@
"dependencies": {
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.1",
"@testing-library/jest-dom": "5.15.1",
"@testing-library/react": "12.1.2",
"@tldraw/vec": "1.4.0",
"@tldraw/vec": "1.1.5",
"@types/jest": "27.0.3",
"@types/pica": "5.1.3",
"@types/react": "17.0.38",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.23.0",
@@ -50,16 +50,16 @@
"react-dom": "17.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.5.2",
"sass": "1.45.2",
"sass": "1.43.5",
"socket.io-client": "2.3.1",
"typescript": "4.5.4"
"typescript": "4.5.2"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/chai": "4.2.22",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3",
"@types/pako": "1.0.2",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4",
"dotenv": "10.0.0",
@@ -68,9 +68,9 @@
"firebase-tools": "9.23.0",
"husky": "7.0.4",
"jest-canvas-mock": "2.3.1",
"lint-staged": "12.1.4",
"lint-staged": "12.1.2",
"pepjs": "0.5.3",
"prettier": "2.5.1",
"prettier": "2.5.0",
"rewire": "5.0.0"
},
"resolutions": {

View File

@@ -14,10 +14,60 @@ import {
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement } from "../element/typeChecks";
import { ExcalidrawImageElement } from "../element/types";
import { imageFromImageData } from "../element/image";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState, _, { canvas, focusContainer }) => {
perform: (
elements,
appState,
_,
{ canvas, focusContainer, imageCache, addFiles },
) => {
if (appState.editingImageElement) {
const { elementId, imageData } = appState.editingImageElement;
const editingImageElement = elements.find((el) => el.id === elementId) as
| ExcalidrawImageElement
| undefined;
if (editingImageElement?.fileId) {
const cachedImageData = imageCache.get(editingImageElement.fileId);
if (cachedImageData) {
const { image, dataURL } = imageFromImageData(imageData);
imageCache.set(editingImageElement.fileId, {
...cachedImageData,
image,
});
addFiles([
{
id: editingImageElement.fileId,
dataURL,
mimeType: cachedImageData.mimeType,
created: Date.now(),
},
]);
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
}
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement;
@@ -162,6 +212,7 @@ export const actionFinalize = register({
keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null ||
appState.editingImageElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),

View File

@@ -17,9 +17,8 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
if (elements.length >= 2) {
@@ -152,12 +151,7 @@ export const actionUngroup = register({
if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false };
}
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
const nextElements = elements.map((element) => {
if (isBoundToContainer(element)) {
boundTextElementIds.push(element.id);
}
const nextGroupIds = removeFromSelectedGroups(
element.groupIds,
appState.selectedGroupIds,
@@ -169,19 +163,11 @@ export const actionUngroup = register({
groupIds: nextGroupIds,
});
});
const updateAppState = selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
);
// remove binded text elements from selection
boundTextElementIds.forEach(
(id) => (updateAppState.selectedElementIds[id] = false),
);
return {
appState: updateAppState,
appState: selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
),
elements: nextElements,
commitToHistory: true,
};

View File

@@ -0,0 +1,75 @@
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { backgroundIcon } from "../components/icons";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { isInitializedImageElement } from "../element/typeChecks";
import Scene from "../scene/Scene";
export const actionEditImageAlpha = register({
name: "editImageAlpha",
perform: async (elements, appState, _, app) => {
if (appState.editingImageElement) {
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(elements, appState);
const selectedElement = selectedElements[0];
if (
selectedElements.length === 1 &&
isInitializedImageElement(selectedElement)
) {
const imgData = app.imageCache.get(selectedElement.fileId);
if (!imgData) {
return false;
}
const image = await imgData.image;
const { width, height } = image;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
Scene.mapElementToScene(selectedElement.id, app.scene);
return {
appState: {
...appState,
editingImageElement: {
editorType: "alpha",
elementId: selectedElement.id,
origImageData: imageData,
imageData,
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
},
},
commitToHistory: false,
};
}
return false;
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
icon={backgroundIcon}
label="Edit Image Alpha"
className={appState.editingImageElement ? "active" : ""}
title={"Edit image alpha"}
aria-label={"Edit image alpha"}
onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
/>
),
});

View File

@@ -42,7 +42,6 @@ import {
redrawTextBoundingBox,
} from "../element";
import { newElementWith } from "../element/mutateElement";
import { getBoundTextElement } from "../element/textElement";
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
import {
Arrowhead,
@@ -58,27 +57,20 @@ import {
canChangeSharpness,
canHaveArrowheads,
getCommonAttributeOfSelectedElements,
getSelectedElements,
getTargetElements,
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import Scene from "../scene/Scene";
import { arrayToMap } from "../utils";
import { register } from "./register";
const changeProperty = (
elements: readonly ExcalidrawElement[],
appState: AppState,
callback: (element: ExcalidrawElement) => ExcalidrawElement,
includeBoundText = false,
) => {
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, includeBoundText),
);
return elements.map((element) => {
if (
selectedElementIds.get(element.id) ||
appState.selectedElementIds[element.id] ||
element.id === appState.editingElement?.id
) {
return callback(element);
@@ -434,26 +426,17 @@ export const actionChangeFontSize = register({
name: "changeFontSize",
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontSize: value,
});
let container = null;
if (el.containerId) {
container = Scene.getScene(el)!.getElement(el.containerId);
}
redrawTextBoundingBox(element, container, appState);
return element;
}
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontSize: value,
});
redrawTextBoundingBox(element);
return element;
}
return el;
},
true,
),
return el;
}),
appState: {
...appState,
currentItemFontSize: value,
@@ -491,16 +474,7 @@ export const actionChangeFontSize = register({
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
(element) => isTextElement(element) && element.fontSize,
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)}
onChange={(value) => updateData(value)}
@@ -513,26 +487,17 @@ export const actionChangeFontFamily = register({
name: "changeFontFamily",
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontFamily: value,
});
let container = null;
if (el.containerId) {
container = Scene.getScene(el)!.getElement(el.containerId);
}
redrawTextBoundingBox(element, container, appState);
return element;
}
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontFamily: value,
});
redrawTextBoundingBox(element);
return element;
}
return el;
},
true,
),
return el;
}),
appState: {
...appState,
currentItemFontFamily: value,
@@ -572,16 +537,7 @@ export const actionChangeFontFamily = register({
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontFamily;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontFamily;
}
return null;
},
(element) => isTextElement(element) && element.fontFamily,
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
)}
onChange={(value) => updateData(value)}
@@ -595,26 +551,17 @@ export const actionChangeTextAlign = register({
name: "changeTextAlign",
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
textAlign: value,
});
let container = null;
if (el.containerId) {
container = Scene.getScene(el)!.getElement(el.containerId);
}
redrawTextBoundingBox(element, container, appState);
return element;
}
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
textAlign: value,
});
redrawTextBoundingBox(element);
return element;
}
return el;
},
true,
),
return el;
}),
appState: {
...appState,
currentItemTextAlign: value,
@@ -647,16 +594,7 @@ export const actionChangeTextAlign = register({
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
(element) => isTextElement(element) && element.textAlign,
appState.currentItemTextAlign,
)}
onChange={(value) => updateData(value)}

View File

@@ -12,9 +12,6 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import Scene from "../scene/Scene";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawTextElement } from "../element/types";
// `copiedStyles` is exported only for tests.
export let copiedStyles: string = "{}";
@@ -64,18 +61,7 @@ export const actionPasteStyles = register({
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
});
let container = null;
if (isBoundToContainer(element)) {
container = Scene.getScene(element)!.getElement(
element.containerId,
);
}
redrawTextBoundingBox(
element as ExcalidrawTextElement,
container,
appState,
);
redrawTextBoundingBox(newElement);
}
return newElement;
}

View File

@@ -80,3 +80,4 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionEditImageAlpha } from "./actionImageEditing";

View File

@@ -101,7 +101,8 @@ export type ActionName =
| "flipVertical"
| "viewMode"
| "exportWithDarkMode"
| "toggleTheme";
| "toggleTheme"
| "editImageAlpha";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@@ -1,7 +1,6 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { Box, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
position: "start" | "center" | "end";
@@ -31,6 +30,28 @@ export const alignElements = (
});
};
export const getMaximumGroups = (
elements: ExcalidrawElement[],
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
? element.id
: element.groupIds[element.groupIds.length - 1];
const currentGroupMembers = groups.get(groupId) || [];
groups.set(groupId, [...currentGroupMembers, element]);
});
return Array.from(groups.values());
};
const calculateTranslation = (
group: ExcalidrawElement[],
selectionBoundingBox: Box,

View File

@@ -41,6 +41,7 @@ export const getDefaultAppState = (): Omit<
editingElement: null,
editingGroupId: null,
editingLinearElement: null,
editingImageElement: null,
elementLocked: false,
elementType: "selection",
errorMessage: null,
@@ -125,6 +126,7 @@ const APP_STATE_STORAGE_CONF = (<
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
editingImageElement: { browser: false, export: false, server: false },
elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },

View File

@@ -19,6 +19,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { isImageElement } from "../element/typeChecks";
export const SelectedShapeActions = ({
appState,
@@ -105,6 +106,13 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</>
)}
<fieldset>
<div className="buttonList">
{targetElements.some((element) => isImageElement(element)) &&
renderAction("editImageAlpha")}
</div>
</fieldset>
{renderAction("changeOpacity")}
<fieldset>

View File

@@ -123,7 +123,6 @@ import {
hasBoundTextElement,
isBindingElement,
isBindingElementType,
isBoundToContainer,
isImageElement,
isInitializedImageElement,
isLinearElement,
@@ -238,6 +237,7 @@ import {
getBoundTextElementId,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { ImageEditor } from "../element/imageEditor";
const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext);
@@ -282,7 +282,7 @@ class App extends React.Component<AppProps, AppState> {
UIOptions: DEFAULT_UI_OPTIONS,
};
private scene: Scene;
public scene: Scene;
private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"];
@@ -918,16 +918,8 @@ class App extends React.Component<AppProps, AppState> {
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
window.removeEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
window.removeEventListener(EVENT.DROP, this.disableEvent, false);
document.removeEventListener(
EVENT.GESTURE_START,
@@ -996,16 +988,8 @@ class App extends React.Component<AppProps, AppState> {
window.addEventListener(EVENT.RESIZE, this.onResize, false);
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
window.addEventListener(EVENT.BLUR, this.onBlur, false);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
window.addEventListener(EVENT.DROP, this.disableEvent, false);
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
@@ -1048,8 +1032,14 @@ class App extends React.Component<AppProps, AppState> {
);
if (
this.state.editingLinearElement &&
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
(this.state.editingLinearElement &&
!this.state.selectedElementIds[
this.state.editingLinearElement.elementId
]) ||
(this.state.editingImageElement &&
!this.state.selectedElementIds[
this.state.editingImageElement.elementId
])
) {
// defer so that the commitToHistory flag isn't reset via current update
setTimeout(() => {
@@ -1152,6 +1142,7 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.isMobile,
editingImageElement: this.state.editingImageElement,
},
);
if (scrollBars) {
@@ -1420,7 +1411,7 @@ class App extends React.Component<AppProps, AppState> {
...this.state,
isLibraryOpen: false,
selectedElementIds: newElements.reduce((map, element) => {
if (!isBoundToContainer(element)) {
if (isTextElement(element) && !element.containerId) {
map[element.id] = true;
}
return map;
@@ -1680,11 +1671,9 @@ class App extends React.Component<AppProps, AppState> {
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT);
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
true,
);
const selectedElements = this.scene
.getElements()
.filter((element) => this.state.selectedElementIds[element.id]);
let offsetX = 0;
let offsetY = 0;
@@ -1765,7 +1754,6 @@ class App extends React.Component<AppProps, AppState> {
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
isHoldingSpace = true;
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
event.preventDefault();
}
if (event.key === KEYS.G || event.key === KEYS.S) {
@@ -2086,7 +2074,7 @@ class App extends React.Component<AppProps, AppState> {
/** whether to attempt to insert at element center if applicable */
insertAtParentCenter?: boolean;
}) => {
let parentCenterPosition =
const parentCenterPosition =
insertAtParentCenter &&
this.getTextWysiwygSnappedToCenterPosition(
sceneX,
@@ -2101,9 +2089,10 @@ class App extends React.Component<AppProps, AppState> {
const container =
shouldBind || parentCenterPosition
? getElementContainingPosition(
this.scene.getElements().filter((ele) => !isTextElement(ele)),
this.scene.getElements(),
sceneX,
sceneY,
"text",
)
: null;
@@ -2130,15 +2119,6 @@ class App extends React.Component<AppProps, AppState> {
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
if (parentCenterPosition) {
parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
sceneX,
sceneY,
this.state,
this.canvas,
window.devicePixelRatio,
);
}
}
const element = existingTextElement
@@ -2168,7 +2148,6 @@ class App extends React.Component<AppProps, AppState> {
? "middle"
: DEFAULT_VERTICAL_ALIGN,
containerId: container?.id ?? undefined,
groupIds: container?.groupIds ?? [],
});
this.setState({ editingElement: element });
@@ -2359,6 +2338,10 @@ class App extends React.Component<AppProps, AppState> {
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer;
if (this.state.editingImageElement) {
return;
}
if (
this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging
@@ -2732,7 +2715,6 @@ class App extends React.Component<AppProps, AppState> {
return false;
}
isPanning = true;
event.preventDefault();
let nextPastePrevented = false;
const isLinux = /Linux/.test(window.navigator.platform);
@@ -2950,6 +2932,14 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState,
): boolean => {
if (this.state.elementType === "selection") {
if (this.state.editingImageElement) {
ImageEditor.handlePointerDown(
this.state.editingImageElement,
pointerDownState.origin,
);
return false;
}
const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
@@ -3510,6 +3500,22 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (this.state.editingImageElement) {
const newImageData = ImageEditor.handlePointerMove(
this.state.editingImageElement,
pointerCoords,
);
if (newImageData) {
this.setState({
editingImageElement: {
...this.state.editingImageElement,
imageData: newImageData,
},
});
}
return;
}
if (this.state.editingLinearElement) {
const didDrag = LinearElementEditor.handlePointDragging(
this.state,
@@ -3579,7 +3585,6 @@ class App extends React.Component<AppProps, AppState> {
lockDirection,
dragDistanceX,
dragDistanceY,
this.state,
);
this.maybeSuggestBindingForAll(selectedElements);
@@ -3833,6 +3838,10 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
if (this.state.editingImageElement) {
ImageEditor.handlePointerUp(this.state.editingImageElement);
}
// Handle end of dragging a point of a linear element, might close a loop
// and sets binding element
if (this.state.editingLinearElement) {

View File

@@ -260,18 +260,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[
getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[
getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`),
]}
/>
<Shortcut
label={t("labels.moveCanvas")}
shortcuts={[

View File

@@ -61,27 +61,6 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
return t("hints.rotate");
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingElement && isTextElement(appState.editingElement)) {
return t("hints.text_editing");
}
if (elementType === "selection") {
if (
appState.draggingElement?.type === "selection" &&
!appState.editingElement &&
!appState.editingLinearElement
) {
return t("hints.deepBoxSelect");
}
if (!selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
}
if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) {
@@ -96,6 +75,18 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
}
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingElement && isTextElement(appState.editingElement)) {
return t("hints.text_editing");
}
if (elementType === "selection" && !selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
return null;
};

View File

@@ -27,8 +27,6 @@
.library-unit__dragger {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}

View File

@@ -89,6 +89,14 @@ export const trash = createIcon(
{ width: 448, height: 512 },
);
export const backgroundIcon = createIcon(
<path
fill="currentColor"
d="M512 320s-64 92.65-64 128c0 35.35 28.66 64 64 64s64-28.65 64-64-64-128-64-128zm-9.37-102.94L294.94 9.37C288.69 3.12 280.5 0 272.31 0s-16.38 3.12-22.62 9.37l-81.58 81.58L81.93 4.76c-6.25-6.25-16.38-6.25-22.62 0L36.69 27.38c-6.24 6.25-6.24 16.38 0 22.62l86.19 86.18-94.76 94.76c-37.49 37.48-37.49 98.26 0 135.75l117.19 117.19c18.74 18.74 43.31 28.12 67.87 28.12 24.57 0 49.13-9.37 67.87-28.12l221.57-221.57c12.5-12.5 12.5-32.75.01-45.25zm-116.22 70.97H65.93c1.36-3.84 3.57-7.98 7.43-11.83l13.15-13.15 81.61-81.61 58.6 58.6c12.49 12.49 32.75 12.49 45.24 0s12.49-32.75 0-45.24l-58.6-58.6 58.95-58.95 162.44 162.44-48.34 48.34z"
></path>,
{ width: 576, height: 512 },
);
export const palette = createIcon(
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",

View File

@@ -182,4 +182,4 @@ export const VERSIONS = {
excalidrawLibrary: 2,
} as const;
export const BOUND_TEXT_PADDING = 5;
export const PADDING = 30;

View File

@@ -1,7 +1,17 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { getMaximumGroups } from "./groups";
import { getCommonBoundingBox } from "./element/bounds";
import { getCommonBounds } from "./element";
interface Box {
minX: number;
minY: number;
maxX: number;
maxY: number;
midX: number;
midY: number;
width: number;
height: number;
}
export interface Distribution {
space: "between";
@@ -88,3 +98,39 @@ export const distributeElements = (
);
});
};
export const getMaximumGroups = (
elements: ExcalidrawElement[],
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
? element.id
: element.groupIds[element.groupIds.length - 1];
const currentGroupMembers = groups.get(groupId) || [];
groups.set(groupId, [...currentGroupMembers, element]);
});
return Array.from(groups.values());
};
const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
midX: (minX + maxX) / 2,
midY: (minY + maxY) / 2,
};
};

View File

@@ -520,24 +520,11 @@ export interface Box {
minY: number;
maxX: number;
maxY: number;
midX: number;
midY: number;
width: number;
height: number;
}
export const getCommonBoundingBox = (
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
midX: (minX + maxX) / 2,
midY: (minY + maxY) / 2,
};
return { minX, minY, maxX, maxY };
};

View File

@@ -46,7 +46,8 @@ const isElementDraggableFromInside = (
return true;
}
const isDraggableFromInside =
!isTransparent(element.backgroundColor) || hasBoundTextElement(element);
!isTransparent(element.backgroundColor) ||
(isTransparent(element.backgroundColor) && hasBoundTextElement(element));
if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points);
}

View File

@@ -5,9 +5,8 @@ import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types";
import { PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement";
import { isSelectedViaGroup } from "../groups";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
@@ -17,7 +16,6 @@ export const dragSelectedElements = (
lockDirection: boolean = false,
distanceX: number = 0,
distanceY: number = 0,
appState: AppState,
) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
@@ -30,15 +28,7 @@ export const dragSelectedElements = (
element,
offset,
);
// update coords of bound text only if we're dragging the container directly
// (we don't drag the group that it's part of)
if (
// container isn't part of any group
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
!element.groupIds.length ||
// container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
) {
if (!element.groupIds.length) {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =

View File

@@ -109,3 +109,16 @@ export const normalizeSVG = async (SVGString: string) => {
return svg.outerHTML;
}
};
export const imageFromImageData = (imagedata: ImageData) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = imagedata.width;
canvas.height = imagedata.height;
ctx.putImageData(imagedata, 0, 0);
const image = new Image();
const dataURL = canvas.toDataURL() as DataURL;
image.src = dataURL;
return { image, dataURL };
};

112
src/element/imageEditor.ts Normal file
View File

@@ -0,0 +1,112 @@
import { distance2d } from "../math";
import Scene from "../scene/Scene";
import {
ExcalidrawImageElement,
InitializedExcalidrawImageElement,
} from "./types";
export type EditingImageElement = {
editorType: "alpha";
elementId: ExcalidrawImageElement["id"];
origImageData: Readonly<ImageData>;
imageData: ImageData;
pointerDownState: {
screenX: number;
screenY: number;
sampledPixel: readonly [number, number, number, number] | null;
};
};
const getElement = (id: EditingImageElement["elementId"]) => {
const element = Scene.getScene(id)?.getNonDeletedElement(id);
if (element) {
return element as InitializedExcalidrawImageElement;
}
return null;
};
export class ImageEditor {
static handlePointerDown(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const imageElement = getElement(editingElement.elementId);
if (imageElement) {
if (
scenePointer.x >= imageElement.x &&
scenePointer.x <= imageElement.x + imageElement.width &&
scenePointer.y >= imageElement.y &&
scenePointer.y <= imageElement.y + imageElement.height
) {
editingElement.pointerDownState.screenX = scenePointer.x;
editingElement.pointerDownState.screenY = scenePointer.y;
const { width, height, data } = editingElement.origImageData;
const imageOffsetX = Math.round(
(scenePointer.x - imageElement.x) * (width / imageElement.width),
);
const imageOffsetY = Math.round(
(scenePointer.y - imageElement.y) * (height / imageElement.height),
);
const sampledPixel = [
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
] as const;
editingElement.pointerDownState.sampledPixel = sampledPixel;
}
}
}
static handlePointerMove(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const { sampledPixel } = editingElement.pointerDownState;
if (sampledPixel) {
const { screenX, screenY } = editingElement.pointerDownState;
const distance = distance2d(
scenePointer.x,
scenePointer.y,
screenX,
screenY,
);
const { width, height, data } = editingElement.origImageData;
const newImageData = new ImageData(width, height);
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
if (
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
distance
) {
newImageData.data[(y * width + x) * 4 + 0] = 0;
newImageData.data[(y * width + x) * 4 + 1] = 255;
newImageData.data[(y * width + x) * 4 + 2] = 0;
newImageData.data[(y * width + x) * 4 + 3] = 0;
} else {
for (let p = 0; p < 4; ++p) {
newImageData.data[(y * width + x) * 4 + p] =
data[(y * width + x) * 4 + p];
}
}
}
}
return newImageData;
}
}
static handlePointerUp(editingElement: EditingImageElement) {
editingElement.pointerDownState.sampledPixel = null;
editingElement.origImageData = editingElement.imageData;
}
}

View File

@@ -13,7 +13,7 @@ import {
FontFamilyValues,
ExcalidrawRectangleElement,
} from "../element/types";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
import { getFontString, getUpdatedTimestamp } from "../utils";
import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
@@ -24,7 +24,7 @@ import { getResizedElementAbsoluteCoords } from "./bounds";
import { measureText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import Scene from "../scene/Scene";
import { BOUND_TEXT_PADDING } from "../constants";
import { PADDING } from "../constants";
type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -219,11 +219,11 @@ const getAdjustedDimensions = (
const container = Scene.getScene(element)!.getElement(element.containerId)!;
let height = container.height;
let width = container.width;
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
height = nextHeight + BOUND_TEXT_PADDING * 2;
if (nextHeight > height - PADDING * 2) {
height = nextHeight + PADDING * 2;
}
if (nextWidth > width - BOUND_TEXT_PADDING * 2) {
width = nextWidth + BOUND_TEXT_PADDING * 2;
if (nextWidth > width - PADDING * 2) {
width = nextWidth + PADDING * 2;
}
if (height !== container.height || width !== container.width) {
mutateElement(container, { height, width });
@@ -369,7 +369,7 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
overrides?: Partial<TElement>,
): TElement => {
let copy: TElement = deepCopyElement(element);
if (isTestEnv()) {
if (process.env.NODE_ENV === "test") {
copy.id = `${copy.id}_copy`;
// `window.h` may not be defined in some unit tests
if (

View File

@@ -1,140 +0,0 @@
import { wrapText } from "./textElement";
import { FontString } from "./types";
describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
describe("When text doesn't contain new lines", () => {
const text = "Hello whats up";
[
{
desc: "break all words when width of each word is less than container width",
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
width: 25,
res: `H
e
l
l
o
w
h
a
t
s
u
p`,
},
{
desc: "break words as per the width",
width: 150,
res: `Hello whats
up`,
},
{
desc: "fit the container",
width: 250,
res: "Hello whats up",
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
describe("When text contain new lines", () => {
const text = `Hello
whats up`;
[
{
desc: "break all words when width of each word is less than container width",
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
width: 25,
res: `H
e
l
l
o
w
h
a
t
s
u
p`,
},
{
desc: "break words as per the width",
width: 150,
res: `Hello
whats up`,
},
{
desc: "fit the container",
width: 250,
res: `Hello
whats up`,
},
].forEach((data) => {
it(`should respect new lines and ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
describe("When text is long", () => {
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
[
{
desc: "fit characters of long string as per container width",
width: 170,
res: `hellolongtextth
isiswhatsupwith
youIamtypingggg
gandtypinggg
break it now`,
},
{
desc: "fit characters of long string as per container width and break words as per the width",
width: 130,
res: `hellolongte
xtthisiswha
tsupwithyou
Iamtypinggg
ggandtyping
gg break it
now`,
},
{
desc: "fit the long text when container width is greater than text length and move the rest to next line",
width: 600,
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
break it now`,
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
});

View File

@@ -1,34 +1,20 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils";
import { getFontString, arrayToMap } from "../utils";
import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement";
import { BOUND_TEXT_PADDING } from "../constants";
import { PADDING } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { AppState } from "../types";
export const redrawTextBoundingBox = (
element: ExcalidrawTextElement,
container: ExcalidrawElement | null,
appState: AppState,
) => {
const maxWidth = container
? container.width - BOUND_TEXT_PADDING * 2
: undefined;
let text = element.text;
if (container) {
text = wrapText(
element.originalText,
getFontString(element),
container.width,
);
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
let maxWidth;
if (element.containerId) {
maxWidth = element.width;
}
const metrics = measureText(
element.originalText,
@@ -36,24 +22,10 @@ export const redrawTextBoundingBox = (
maxWidth,
);
let coordY = element.y;
// Resize container and vertically center align the text
if (container) {
coordY = container.y + container.height / 2 - metrics.height / 2;
let nextHeight = container.height;
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
coordY = container.y + nextHeight / 2 - metrics.height / 2;
}
mutateElement(container, { height: nextHeight });
}
mutateElement(element, {
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
y: coordY,
text,
});
};
@@ -110,12 +82,20 @@ export const handleBindTextResize = (
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
let minCharWidthTillNow = 0;
if (text) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
minCharWidthTillNow = getMinCharWidth(getFontString(textElement));
// check if the diff has exceeded min char width needed
const diff = Math.abs(
element.width - textElement.width + PADDING * 2,
);
if (diff >= minCharWidthTillNow) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
}
}
const dimensions = measureText(
@@ -127,8 +107,8 @@ export const handleBindTextResize = (
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
if (nextHeight > element.height - PADDING * 2) {
containerHeight = nextHeight + PADDING * 2;
const diff = containerHeight - element.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
@@ -147,9 +127,9 @@ export const handleBindTextResize = (
mutateElement(textElement, {
text,
// preserve padding and set width correctly
width: element.width - BOUND_TEXT_PADDING * 2,
width: element.width - PADDING * 2,
height: nextHeight,
x: element.x + BOUND_TEXT_PADDING,
x: element.x + PADDING,
y: updatedY,
baseline: nextBaseLine,
});
@@ -218,12 +198,6 @@ const getTextWidth = (text: string, font: FontString) => {
canvas2dContext.font = font;
const metrics = canvas2dContext.measureText(text);
// since in test env the canvas measureText algo
// doesn't measure text and instead just returns number of
// characters hence we assume that each letteris 10px
if (isTestEnv()) {
return metrics.width * 10;
}
return metrics.width;
};
@@ -233,7 +207,7 @@ export const wrapText = (
font: FontString,
containerWidth: number,
) => {
const maxWidth = containerWidth - BOUND_TEXT_PADDING * 2;
const maxWidth = containerWidth - PADDING * 2;
const lines: Array<string> = [];
const originalLines = text.split("\n");
@@ -252,7 +226,7 @@ export const wrapText = (
const currentWordWidth = getTextWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth >= maxWidth) {
if (currentWordWidth > maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
if (currentLine) {
@@ -283,7 +257,7 @@ export const wrapText = (
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
if (currentLineWidthTillNow + spaceWidth > maxWidth) {
lines.push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
@@ -311,15 +285,8 @@ export const wrapText = (
}
index++;
currentLine += `${word} `;
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
lines.push(currentLine.slice(0, -1));
currentLine = "";
currentLineWidthTillNow = 0;
break;
}
}
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
@@ -353,23 +320,34 @@ export const charWidth = (() => {
return cachedCharWidth[font][ascii];
};
const updateCache = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font][ascii]) {
cachedCharWidth[font][ascii] = calculate(char, font);
}
};
const clearCacheforFont = (font: FontString) => {
cachedCharWidth[font] = [];
};
const getCache = (font: FontString) => {
return cachedCharWidth[font];
};
return {
calculate,
updateCache,
clearCacheforFont,
getCache,
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
return (
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
BOUND_TEXT_PADDING * 2
);
return measureText(DUMMY_TEXT.split("").join("\n"), font).width + PADDING * 2;
};
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
return getApproxLineHeight(font) + PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => {
@@ -409,16 +387,3 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
};
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
if (!element) {
return null;
}
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
return Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
}
return null;
};

View File

@@ -2,11 +2,12 @@ import { CODES, KEYS } from "../keys";
import {
isWritableElement,
getFontString,
viewportCoordsToSceneCoords,
getFontFamilyString,
} from "../utils";
import Scene from "../scene/Scene";
import { isBoundToContainer, isTextElement } from "./typeChecks";
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
import { CLASSES, PADDING } from "../constants";
import {
ExcalidrawBindableElement,
ExcalidrawElement,
@@ -36,20 +37,16 @@ const getTransform = (
angle: number,
appState: AppState,
maxWidth: number,
maxHeight: number,
) => {
const { zoom, offsetTop, offsetLeft } = appState;
const degree = (180 * angle) / Math.PI;
// offsets must be multiplied by 2 to account for the division by 2 of
// the whole expression afterwards
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
let translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
const translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
if (width > maxWidth && zoom.value !== 1) {
translateX = (maxWidth / 2) * (zoom.value - 1);
}
if (height > maxHeight && zoom.value !== 1) {
translateY = ((maxHeight - offsetTop * 2) * (zoom.value - 1)) / 2;
}
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
};
@@ -102,68 +99,83 @@ export const textWysiwyg = ({
if (updatedElement && isTextElement(updatedElement)) {
let coordX = updatedElement.x;
let coordY = updatedElement.y;
const container = updatedElement?.containerId
let container = updatedElement?.containerId
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
: null;
let maxWidth = updatedElement.width;
let maxHeight = updatedElement.height;
let width = updatedElement.width;
// Set to element height by default since thats
// what is going to be used for unbounded text
let height = updatedElement.height;
if (container && updatedElement.containerId) {
const propertiesUpdated = textPropertiesUpdated(
updatedElement,
editable,
);
// using editor.style.height to get the accurate height of text editor
const editorHeight = Number(editable.style.height.slice(0, -2));
if (editorHeight > 0) {
height = editorHeight;
}
if (propertiesUpdated) {
const currentContainer = Scene.getScene(updatedElement)?.getElement(
updatedElement.containerId,
) as ExcalidrawBindableElement;
approxLineHeight = isTextElement(updatedElement)
? getApproxLineHeight(getFontString(updatedElement))
: 0;
originalContainerHeight = container.height;
// update height of the editor after properties updated
height = updatedElement.height;
if (updatedElement.height > currentContainer.height - PADDING * 2) {
const nextHeight = updatedElement.height + PADDING * 2;
originalContainerHeight = nextHeight;
mutateElement(container, { height: nextHeight });
container = { ...container, height: nextHeight };
}
editable.style.height = `${updatedElement.height}px`;
}
if (!originalContainerHeight) {
originalContainerHeight = container.height;
}
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
maxWidth = container.width - PADDING * 2;
maxHeight = container.height - PADDING * 2;
width = maxWidth;
height = Math.min(height, maxHeight);
// The coordinates of text box set a distance of
// 30px to preserve padding
coordX = container.x + BOUND_TEXT_PADDING;
coordX = container.x + PADDING;
// autogrow container height if text exceeds
if (height > maxHeight) {
const diff = Math.min(height - maxHeight, approxLineHeight);
if (editable.clientHeight > maxHeight) {
const diff = Math.min(
editable.clientHeight - maxHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height + diff });
return;
} else if (
// autoshrink container height until original container height
// is reached when text is removed
container.height > originalContainerHeight &&
height < maxHeight
editable.clientHeight < maxHeight
) {
const diff = Math.min(maxHeight - height, approxLineHeight);
const diff = Math.min(
maxHeight - editable.clientHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height - diff });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
// vertically center align the text
coordY = container.y + container.height / 2 - height / 2;
const lines = editable.clientHeight / approxLineHeight;
// For some reason the scrollHeight gets set to twice the lineHeight
// when you start typing for first time and thus line count is 2
// hence this check
if (lines > 2 || propertiesUpdated) {
// vertically center align the text
coordY =
container.y + container.height / 2 - editable.clientHeight / 2;
}
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const { textAlign } = updatedElement;
const { textAlign, angle } = updatedElement;
editable.value = updatedElement.originalText || updatedElement.text;
const lines = updatedElement.originalText.split("\n");
const lineHeight = updatedElement.containerId
@@ -180,36 +192,19 @@ export const textWysiwyg = ({
).marginRight.slice(0, -2),
);
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height -
viewportY -
// There is a ~14px difference which keeps on increasing
// with every zoom step when offset present hence I am subtracting it here
// However this is not the best fix and breaks in
// few scenarios
(appState.offsetTop
? ((appState.zoom.value * 100 - 100) / 10) * 14
: 0)) /
(appState.offsetTop + appState.height - viewportY) /
appState.zoom.value;
const angle = container ? container.angle : updatedElement.angle;
Object.assign(editable.style, {
font: getFontString(updatedElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`,
width: `${width}px`,
height: `${height}px`,
height: `${Math.max(editable.clientHeight, updatedElement.height)}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(
width,
height,
angle,
appState,
maxWidth,
editorMaxHeight,
),
transform: getTransform(width, height, angle, appState, maxWidth),
textAlign,
color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100,
@@ -259,37 +254,8 @@ export const textWysiwyg = ({
if (onChange) {
editable.oninput = () => {
// using scrollHeight here since we need to calculate
// number of lines so cannot use editable.style.height
// as that gets updated below
const lines = editable.scrollHeight / approxLineHeight;
// auto increase height only when lines > 1 so its
// measured correctly and vertically alignes for
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
let height = "auto";
if (lines === 2) {
const container = Scene.getScene(element)!.getElement(
element.containerId,
);
const actualLineCount = wrapText(
editable.value,
getFontString(element),
container!.width,
).split("\n").length;
// This is browser behaviour when setting height to "auto"
// It sets the height needed for 2 lines even if actual
// line count is 1 as mentioned above as well
// hence reducing the height by half if actual line count is 1
// so single line aligns vertically when deleting
if (actualLineCount === 1) {
height = `${editable.scrollHeight / 2}px`;
}
}
editable.style.height = height;
if (isBoundToContainer(element)) {
editable.style.height = "auto";
editable.style.height = `${editable.scrollHeight}px`;
}
onChange(normalizeText(editable.value));
@@ -450,17 +416,20 @@ export const textWysiwyg = ({
getFontString(updateElement),
container.width,
);
const { x, y } = viewportCoordsToSceneCoords(
{
clientX: Number(editable.style.left.slice(0, -2)),
clientY: Number(editable.style.top.slice(0, -2)),
},
appState,
);
if (isTextElement(updateElement) && updateElement.containerId) {
const editorHeight = Number(editable.style.height.slice(0, -2));
if (editable.value) {
mutateElement(updateElement, {
// vertically center align
y: container.y + container.height / 2 - editorHeight / 2,
height: editorHeight,
y: y + appState.offsetTop,
height: Number(editable.style.height.slice(0, -2)),
width: Number(editable.style.width.slice(0, -2)),
// preserve padding
x: container.x + BOUND_TEXT_PADDING,
angle: container.angle,
x: x + appState.offsetLeft,
});
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {

View File

@@ -1,13 +1,6 @@
import {
GroupId,
ExcalidrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./element/types";
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
import { AppState } from "./types";
import { getSelectedElements } from "./scene";
import { getBoundTextElementId } from "./element/textElement";
import Scene from "./scene/Scene";
export const selectGroup = (
groupId: GroupId,
@@ -165,33 +158,3 @@ export const removeFromSelectedGroups = (
groupIds: ExcalidrawElement["groupIds"],
selectedGroupIds: { [groupId: string]: boolean },
) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);
export const getMaximumGroups = (
elements: ExcalidrawElement[],
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
? element.id
: element.groupIds[element.groupIds.length - 1];
const currentGroupMembers = groups.get(groupId) || [];
// Include bounded text if present when grouping
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
currentGroupMembers.push(textElement);
}
groups.set(groupId, [...currentGroupMembers, element]);
});
return Array.from(groups.values());
};

View File

@@ -208,8 +208,7 @@
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
"placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
"bindTextToElement": "Press enter to add text"
},
"canvasError": {
"cannotShowPreview": "Cannot show preview",
@@ -256,8 +255,6 @@
"helpDialog": {
"blog": "Read our blog",
"click": "click",
"deepSelect": "Deep select",
"deepBoxSelect": "Deep select within box, and prevent dragging",
"curvedArrow": "Curved arrow",
"curvedLine": "Curved line",
"documentation": "Documentation",

View File

@@ -64,15 +64,8 @@ Please add the latest change on the top under the correct section.
The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used.
### Fixes
- Panning the canvas using `mousewheel-drag` and `space-drag` now prevents the browser from scrolling the container/page [#4489](https://github.com/excalidraw/excalidraw/pull/4489).
- Scope drag and drop events to Excalidraw container to prevent overriding host application drag and drop events.
### Build
- Added an example to test and develop the package [locally](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Development) using `yarn start`
- Remove `file-loader` so font assets are not duplicated by webpack and use webpack asset modules for font generation [#4380](https://github.com/excalidraw/excalidraw/pull/4380)
- We're now compiling to `es2017` target. Notably, `async/await` is not compiled down to generators. [#4341](https://github.com/excalidraw/excalidraw/pull/4341)

View File

@@ -1008,21 +1008,3 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
### Development
#### Install the dependencies
```bash
yarn
```
#### Start the server
```bash
yarn start
```
[http://localhost:3001](http://localhost:3001) will open in your default browser.
The example is same as the [codesandbox example](https://ehlz3.csb.app/)

View File

@@ -1,249 +0,0 @@
import { useEffect, useState, useRef } from "react";
import InitialData from "./initialData";
import Sidebar from "./sidebar/Sidebar";
import "./App.scss";
import initialData from "./initialData";
// This is so that we use the bundled excalidraw.developement.js file instead
// of the actual source code
const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw;
const Excalidraw = window.Excalidraw.default;
const renderTopRightUI = () => {
return (
<button onClick={() => alert("This is dummy top right UI")}>
{" "}
Click me{" "}
</button>
);
};
const renderFooter = () => {
return (
<button onClick={() => alert("This is dummy footer")}>
{" "}
custom footer{" "}
</button>
);
};
export default function App() {
const excalidrawRef = useRef(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
const [gridModeEnabled, setGridModeEnabled] = useState(false);
const [blobUrl, setBlobUrl] = useState(null);
const [canvasUrl, setCanvasUrl] = useState(null);
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
const [shouldAddWatermark, setShouldAddWatermark] = useState(false);
const [theme, setTheme] = useState("light");
useEffect(() => {
const onHashChange = () => {
const hash = new URLSearchParams(window.location.hash.slice(1));
const libraryUrl = hash.get("addLibrary");
if (libraryUrl) {
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
}
};
window.addEventListener("hashchange", onHashChange, false);
return () => {
window.removeEventListener("hashchange", onHashChange);
};
}, []);
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawRef.current.updateScene(sceneData);
};
return (
<div className="App">
<h1> Excalidraw Example</h1>
<Sidebar>
<div className="button-wrapper">
<button className="update-scene" onClick={updateScene}>
Update Scene
</button>
<button
className="reset-scene"
onClick={() => {
excalidrawRef.current.resetScene();
}}
>
Reset Scene
</button>
<label>
<input
type="checkbox"
checked={viewModeEnabled}
onChange={() => setViewModeEnabled(!viewModeEnabled)}
/>
View mode
</label>
<label>
<input
type="checkbox"
checked={zenModeEnabled}
onChange={() => setZenModeEnabled(!zenModeEnabled)}
/>
Zen mode
</label>
<label>
<input
type="checkbox"
checked={gridModeEnabled}
onChange={() => setGridModeEnabled(!gridModeEnabled)}
/>
Grid mode
</label>
<label>
<input
type="checkbox"
checked={theme === "dark"}
onChange={() => {
let newTheme = "light";
if (theme === "light") {
newTheme = "dark";
}
setTheme(newTheme);
}}
/>
Switch to Dark Theme
</label>
</div>
<div className="excalidraw-wrapper">
<Excalidraw
ref={excalidrawRef}
initialData={InitialData}
onChange={(elements, state) =>
console.info("Elements :", elements, "State : ", state)
}
onPointerUpdate={(payload) => console.info(payload)}
onCollabButtonClick={() =>
window.alert("You clicked on collab button")
}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
theme={theme}
name="Custom name of drawing"
UIOptions={{ canvasActions: { loadScene: false } }}
renderTopRightUI={renderTopRightUI}
renderFooter={renderFooter}
/>
</div>
<div className="export-wrapper button-wrapper">
<label className="export-wrapper__checkbox">
<input
type="checkbox"
checked={exportWithDarkMode}
onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
/>
Export with dark mode
</label>
<label className="export-wrapper__checkbox">
<input
type="checkbox"
checked={shouldAddWatermark}
onChange={() => setShouldAddWatermark(!shouldAddWatermark)}
/>
Add Watermark
</label>
<button
onClick={async () => {
const svg = await exportToSvg({
elements: excalidrawRef.current.getSceneElements(),
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
width: 300,
height: 100,
},
embedScene: true,
});
document.querySelector(".export-svg").innerHTML = svg.outerHTML;
}}
>
Export to SVG
</button>
<div className="export export-svg"></div>
<button
onClick={async () => {
const blob = await exportToBlob({
elements: excalidrawRef.current.getSceneElements(),
mimeType: "image/png",
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
},
});
setBlobUrl(window.URL.createObjectURL(blob));
}}
>
Export to Blob
</button>
<div className="export export-blob">
<img src={blobUrl} alt="" />
</div>
<button
onClick={() => {
const canvas = exportToCanvas({
elements: excalidrawRef.current.getSceneElements(),
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
},
});
const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil";
ctx.strokeText("My custom text", 50, 60);
setCanvasUrl(canvas.toDataURL());
}}
>
Export to Canvas
</button>
<div className="export export-canvas">
<img src={canvasUrl} alt="" />
</div>
</div>
</Sidebar>
</div>
);
}

View File

@@ -1,42 +0,0 @@
.App {
font-family: sans-serif;
text-align: center;
}
.button-wrapper button {
z-index: 1;
height: 40px;
max-width: 200px;
margin: 10px;
padding: 5px;
}
.excalidraw .App-menu_top .buttonList {
display: flex;
}
.excalidraw-wrapper {
height: 800px;
margin: 50px;
}
:root[dir="ltr"]
.excalidraw
.layer-ui__wrapper
.zen-mode-transition.App-menu_bottom--transition-left {
transform: none;
}
.excalidraw .panelColumn {
text-align: left;
}
.export-wrapper {
display: flex;
flex-direction: column;
margin: 50px;
&__checkbox {
display: flex;
}
}

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<title>React App</title>
<script>
window.name = "codesandbox";
</script>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<!-- This is so that we use the bundled excalidraw.developement.js file instead
of the actual source code -->
<script src="./excalidraw.development.js"></script>
<script src="./bundle.js"></script>
</body>
</html>

View File

@@ -1,12 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement,
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
import { useState } from "react";
import "./Sidebar.scss";
export default function Sidebar(props) {
const [open, setOpen] = useState(false);
return (
<>
<div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}>
<button className="closebtn" onClick={() => setOpen(false)}>
x
</button>
<div className="sidebar-links">
<button>Dummy Home</button>
<button>Dummy About</button>{" "}
</div>
</div>
<div className={`${open ? "sidebar-open" : ""}`}>
<button
className="openbtn"
onClick={() => {
setOpen(!open);
}}
>
Open Sidebar
</button>
{props.children}
</div>
</>
);
}

View File

@@ -1,66 +0,0 @@
.sidebar {
height: 100%;
width: 0;
position: absolute;
z-index: 1;
top: 0;
left: 0;
background-color: #111;
overflow-x: hidden;
transition: 0.5s;
padding-top: 60px;
&.open {
width: 300px;
}
&-links {
display: flex;
flex-direction: column;
padding: 20px;
button {
padding: 10px;
margin: 10px;
background: #faa2c1;
color: #fff;
border: none;
cursor: pointer;
}
}
}
.sidebar a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 25px;
color: #818181;
display: block;
transition: 0.3s;
}
.sidebar a:hover {
color: #f1f1f1;
}
.sidebar .closebtn {
position: absolute;
top: 0;
right: 0;
font-size: 36px;
margin-left: 50px;
}
.openbtn {
font-size: 20px;
cursor: pointer;
background-color: #111;
color: white;
padding: 10px 15px;
border: none;
display: flex;
margin-left: 50px;
}
.sidebar-open {
margin-left: 300px;
}

View File

@@ -45,13 +45,13 @@
},
"devDependencies": {
"@babel/core": "7.16.0",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.0",
"@babel/plugin-transform-async-to-generator": "7.16.0",
"@babel/plugin-transform-runtime": "7.16.4",
"@babel/plugin-transform-typescript": "7.16.1",
"@babel/preset-env": "7.16.7",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@babel/preset-env": "7.16.4",
"@babel/preset-react": "7.16.0",
"@babel/preset-typescript": "7.16.0",
"autoprefixer": "10.4.0",
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
@@ -60,14 +60,12 @@
"mini-css-extract-plugin": "2.4.5",
"postcss-loader": "6.2.1",
"sass-loader": "12.4.0",
"terser-webpack-plugin": "5.3.0",
"terser-webpack-plugin": "5.2.5",
"ts-loader": "9.2.6",
"typescript": "4.5.3",
"webpack": "5.65.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "4.7.1",
"webpack-merge": "5.8.0"
"webpack-cli": "4.9.1"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
@@ -75,8 +73,7 @@
"gen:types": "tsc --project ../../../tsconfig-types.json",
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack",
"start": "webpack serve --config webpack.dev-server.config.js "
"pack": "yarn build:umd && yarn pack"
},
"dependencies": {
"dotenv": "10.0.0"

View File

@@ -1,28 +0,0 @@
const path = require("path");
const { merge } = require("webpack-merge");
const devConfig = require("./webpack.dev.config");
const devServerConfig = {
entry: {
bundle: "./example/index.js",
},
// Server Configuration options
devServer: {
port: 3001,
host: "localhost",
hot: true,
compress: true,
static: {
directory: path.join(__dirname, "example"),
},
client: {
progress: true,
logging: "info",
overlay: true, //Shows a full-screen overlay in the browser when there are compiler errors or warnings.
},
open: ["./"],
},
};
module.exports = merge(devServerConfig, devConfig);

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,13 @@
]
},
"devDependencies": {
"@babel/core": "7.16.5",
"@babel/core": "7.16.0",
"@babel/plugin-transform-arrow-functions": "7.16.0",
"@babel/plugin-transform-async-to-generator": "7.16.5",
"@babel/plugin-transform-async-to-generator": "7.16.0",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/plugin-transform-typescript": "7.16.1",
"@babel/preset-env": "7.16.4",
"@babel/preset-typescript": "7.16.5",
"@babel/preset-typescript": "7.16.0",
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",

View File

@@ -14,19 +14,19 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e"
integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==
"@babel/core@7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.5.tgz#924aa9e1ae56e1e55f7184c8bf073a50d8677f5c"
integrity sha512-wUcenlLzuWMZ9Zt8S0KmFwGlH6QKRh3vsm/dhDA3CHkiTA45YuG1XkHRcNRl73EFPXDp/d5kVOU0/y7x2w6OaQ==
"@babel/core@7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4"
integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==
dependencies:
"@babel/code-frame" "^7.16.0"
"@babel/generator" "^7.16.5"
"@babel/helper-compilation-targets" "^7.16.3"
"@babel/helper-module-transforms" "^7.16.5"
"@babel/helpers" "^7.16.5"
"@babel/parser" "^7.16.5"
"@babel/generator" "^7.16.0"
"@babel/helper-compilation-targets" "^7.16.0"
"@babel/helper-module-transforms" "^7.16.0"
"@babel/helpers" "^7.16.0"
"@babel/parser" "^7.16.0"
"@babel/template" "^7.16.0"
"@babel/traverse" "^7.16.5"
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
convert-source-map "^1.7.0"
debug "^4.1.0"
@@ -35,10 +35,10 @@
semver "^6.3.0"
source-map "^0.5.0"
"@babel/generator@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.5.tgz#26e1192eb8f78e0a3acaf3eede3c6fc96d22bedf"
integrity sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA==
"@babel/generator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2"
integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==
dependencies:
"@babel/types" "^7.16.0"
jsesc "^2.5.1"
@@ -103,13 +103,6 @@
resolve "^1.14.2"
semver "^6.1.2"
"@babel/helper-environment-visitor@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.5.tgz#f6a7f38b3c6d8b07c88faea083c46c09ef5451b8"
integrity sha512-ODQyc5AnxmZWm/R2W7fzhamOk1ey8gSguo5SGvF0zcB3uUzRpTRmM/jmLSm9bDMyPlvbyJ+PwPEK0BWIoZ9wjg==
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-explode-assignable-expression@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778"
@@ -147,25 +140,25 @@
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==
dependencies:
"@babel/types" "^7.16.7"
"@babel/types" "^7.16.0"
"@babel/helper-module-transforms@^7.16.0", "@babel/helper-module-transforms@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.5.tgz#530ebf6ea87b500f60840578515adda2af470a29"
integrity sha512-CkvMxgV4ZyyioElFwcuWnDCcNIeyqTkCm9BxXZi73RR1ozqlpboqsbGUNvRTflgZtFbbJ1v5Emvm+lkjMYY/LQ==
"@babel/helper-module-transforms@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5"
integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA==
dependencies:
"@babel/helper-environment-visitor" "^7.16.5"
"@babel/helper-module-imports" "^7.16.0"
"@babel/helper-replace-supers" "^7.16.0"
"@babel/helper-simple-access" "^7.16.0"
"@babel/helper-split-export-declaration" "^7.16.0"
"@babel/helper-validator-identifier" "^7.15.7"
"@babel/template" "^7.16.0"
"@babel/traverse" "^7.16.5"
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/helper-optimise-call-expression@^7.16.0":
@@ -175,18 +168,27 @@
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
"@babel/helper-remap-async-to-generator@^7.16.4", "@babel/helper-remap-async-to-generator@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.5.tgz#e706646dc4018942acb4b29f7e185bc246d65ac3"
integrity sha512-X+aAJldyxrOmN9v3FKp+Hu1NO69VWgYgDGq6YDykwRPzxs5f2N+X988CBXS7EQahDU+Vpet5QYMqLk+nsp+Qxw==
"@babel/helper-remap-async-to-generator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.0.tgz#d5aa3b086e13a5fe05238ff40c3a5a0c2dab3ead"
integrity sha512-MLM1IOMe9aQBqMWxcRw8dcb9jlM86NIw7KA0Wri91Xkfied+dE0QuBFSBjMNvqzmS0OSIDsMNC24dBEkPUi7ew==
dependencies:
"@babel/helper-annotate-as-pure" "^7.16.0"
"@babel/helper-wrap-function" "^7.16.5"
"@babel/helper-wrap-function" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/helper-remap-async-to-generator@^7.16.4":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e"
integrity sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.16.0"
"@babel/helper-wrap-function" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/helper-replace-supers@^7.16.0":
@@ -225,33 +227,28 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
"@babel/helper-validator-identifier@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
"@babel/helper-validator-option@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
"@babel/helper-wrap-function@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.5.tgz#0158fca6f6d0889c3fee8a6ed6e5e07b9b54e41f"
integrity sha512-2J2pmLBqUqVdJw78U0KPNdeE2qeuIyKoG4mKV7wAq3mc4jJG282UgjZw4ZYDnqiWQuS3Y3IYdF/AQ6CpyBV3VA==
"@babel/helper-wrap-function@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c"
integrity sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g==
dependencies:
"@babel/helper-function-name" "^7.16.0"
"@babel/template" "^7.16.0"
"@babel/traverse" "^7.16.5"
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/helpers@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.5.tgz#29a052d4b827846dd76ece16f565b9634c554ebd"
integrity sha512-TLgi6Lh71vvMZGEkFuIxzaPsyeYCHQ5jJOOX1f0xXn0uciFuE8cEk0wyBquMcCxBXZ5BJhE2aUB7pnWTD150Tw==
"@babel/helpers@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.0.tgz#875519c979c232f41adfbd43a3b0398c2e388183"
integrity sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ==
dependencies:
"@babel/template" "^7.16.0"
"@babel/traverse" "^7.16.5"
"@babel/traverse" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/highlight@^7.16.0":
@@ -263,10 +260,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.16.0", "@babel/parser@^7.16.5":
version "7.16.6"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314"
integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ==
"@babel/parser@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.0.tgz#cf147d7ada0a3655e79bf4b08ee846f00a00a295"
integrity sha512-TEHWXf0xxpi9wKVyBCmRcSSDjbJ/cl6LUdlbYUHEaNQUJGhreJbZrXT6sR4+fZLxVUJqNRB4KyOvjuy/D9009A==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2":
version "7.16.2"
@@ -524,14 +521,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-transform-async-to-generator@7.16.5", "@babel/plugin-transform-async-to-generator@^7.16.0":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.5.tgz#89c9b501e65bb14c4579a6ce9563f859de9b34e4"
integrity sha512-TMXgfioJnkXU+XRoj7P2ED7rUm5jbnDWwlCuFVTpQboMfbSya5WrmubNBAMlk7KXvywpo8rd8WuYZkis1o2H8w==
"@babel/plugin-transform-async-to-generator@7.16.0", "@babel/plugin-transform-async-to-generator@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz#df12637f9630ddfa0ef9d7a11bc414d629d38604"
integrity sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw==
dependencies:
"@babel/helper-module-imports" "^7.16.0"
"@babel/helper-plugin-utils" "^7.16.5"
"@babel/helper-remap-async-to-generator" "^7.16.5"
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/helper-remap-async-to-generator" "^7.16.0"
"@babel/plugin-transform-block-scoped-functions@^7.16.0":
version "7.16.0"
@@ -715,12 +712,12 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-transform-runtime@^7.14.5":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.7.tgz#1da184cb83a2287a01956c10c60e66dd503c18aa"
integrity sha512-2FoHiSAWkdq4L06uaDN3rS43i6x28desUVxq+zAFuE6kbWYQeiLPJI5IC7Sg9xKYVcrBKSQkVUfH6aeQYbl9QA==
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.4.tgz#f9ba3c7034d429c581e1bd41b4952f3db3c2c7e8"
integrity sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A==
dependencies:
"@babel/helper-module-imports" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/helper-module-imports" "^7.16.0"
"@babel/helper-plugin-utils" "^7.14.5"
babel-plugin-polyfill-corejs2 "^0.3.0"
babel-plugin-polyfill-corejs3 "^0.4.0"
babel-plugin-polyfill-regenerator "^0.3.0"
@@ -762,7 +759,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-transform-typescript@7.16.1", "@babel/plugin-transform-typescript@^7.16.1":
"@babel/plugin-transform-typescript@7.16.1", "@babel/plugin-transform-typescript@^7.16.0":
version "7.16.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.1.tgz#cc0670b2822b0338355bc1b3d2246a42b8166409"
integrity sha512-NO4XoryBng06jjw/qWEU2LhcLJr1tWkhpMam/H4eas/CDKMX/b2/Ylb6EI256Y7+FVPCawwSM1rrJNOpDiz+Lg==
@@ -877,14 +874,14 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/preset-typescript@7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.5.tgz#b86a5b0ae739ba741347d2f58c52f52e63cf1ba1"
integrity sha512-lmAWRoJ9iOSvs3DqOndQpj8XqXkzaiQs50VG/zESiI9D3eoZhGriU675xNCr0UwvsuXrhMAGvyk1w+EVWF3u8Q==
"@babel/preset-typescript@7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.0.tgz#b0b4f105b855fb3d631ec036cdc9d1ffd1fa5eac"
integrity sha512-txegdrZYgO9DlPbv+9QOVpMnKbOtezsLHWsnsRF4AjbSIsVaujrq1qg8HK0mxQpWv0jnejt0yEoW1uWpvbrDTg==
dependencies:
"@babel/helper-plugin-utils" "^7.16.5"
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/helper-validator-option" "^7.14.5"
"@babel/plugin-transform-typescript" "^7.16.1"
"@babel/plugin-transform-typescript" "^7.16.0"
"@babel/runtime@^7.8.4":
version "7.12.13"
@@ -902,18 +899,17 @@
"@babel/parser" "^7.16.0"
"@babel/types" "^7.16.0"
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.5":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.5.tgz#d7d400a8229c714a59b87624fc67b0f1fbd4b2b3"
integrity sha512-FOCODAzqUMROikDYLYxl4nmwiLlu85rNqBML/A5hKRVXG2LV8d0iMqgPzdYTcIpjZEBB7D6UDU9vxRZiriASdQ==
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.0.tgz#965df6c6bfc0a958c1e739284d3c9fa4a6e3c45b"
integrity sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ==
dependencies:
"@babel/code-frame" "^7.16.0"
"@babel/generator" "^7.16.5"
"@babel/helper-environment-visitor" "^7.16.5"
"@babel/generator" "^7.16.0"
"@babel/helper-function-name" "^7.16.0"
"@babel/helper-hoist-variables" "^7.16.0"
"@babel/helper-split-export-declaration" "^7.16.0"
"@babel/parser" "^7.16.5"
"@babel/parser" "^7.16.0"
"@babel/types" "^7.16.0"
debug "^4.1.0"
globals "^11.1.0"
@@ -926,14 +922,6 @@
"@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.7.tgz#4ed19d51f840ed4bd5645be6ce40775fecf03159"
integrity sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==
dependencies:
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@discoveryjs/json-ext@^0.5.0":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"

View File

@@ -1,6 +1,5 @@
import { Random } from "roughjs/bin/math";
import { nanoid } from "nanoid";
import { isTestEnv } from "./utils";
let random = new Random(Date.now());
let testIdBase = 0;
@@ -12,4 +11,5 @@ export const reseed = (seed: number) => {
testIdBase = 0;
};
export const randomId = () => (isTestEnv() ? `id${testIdBase++}` : nanoid());
export const randomId = () =>
process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();

View File

@@ -12,6 +12,7 @@ import {
isLinearElement,
isFreeDrawElement,
isInitializedImageElement,
isImageElement,
} from "../element/typeChecks";
import {
getDiamondPoints,
@@ -221,19 +222,31 @@ const drawElementOnCanvas = (
break;
}
case "image": {
const img = isInitializedImageElement(element)
? renderConfig.imageCache.get(element.fileId)?.image
: undefined;
if (img != null && !(img instanceof Promise)) {
context.drawImage(
img,
0 /* hardcoded for the selection box*/,
0,
element.width,
element.height,
);
if (renderConfig.editingImageElement) {
const { imageData } = renderConfig.editingImageElement;
const imgCanvas = document.createElement("canvas");
imgCanvas.width = imageData.width;
imgCanvas.height = imageData.height;
const imgContext = imgCanvas.getContext("2d")!;
imgContext.putImageData(imageData, 0, 0);
context.drawImage(imgCanvas, 0, 0, element.width, element.height);
} else {
drawImagePlaceholder(element, context, renderConfig.zoom.value);
const img = isInitializedImageElement(element)
? renderConfig.imageCache.get(element.fileId)?.image
: undefined;
if (img != null && !(img instanceof Promise)) {
context.drawImage(
img,
0 /* hardcoded for the selection box*/,
0,
element.width,
element.height,
);
} else {
drawImagePlaceholder(element, context, renderConfig.zoom.value);
}
}
break;
}
@@ -410,23 +423,23 @@ const generateElementShape = (
topY + (rightY - topY) * 0.25
} L ${rightX - (rightX - topX) * 0.25} ${
rightY - (rightY - topY) * 0.25
}
}
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
rightX - (rightX - bottomX) * 0.25
} ${rightY + (bottomY - rightY) * 0.25}
} ${rightY + (bottomY - rightY) * 0.25}
L ${bottomX + (rightX - bottomX) * 0.25} ${
bottomY - (bottomY - rightY) * 0.25
}
}
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
bottomX - (bottomX - leftX) * 0.25
} ${bottomY - (bottomY - leftY) * 0.25}
} ${bottomY - (bottomY - leftY) * 0.25}
L ${leftX + (bottomX - leftX) * 0.25} ${
leftY + (bottomY - leftY) * 0.25
}
}
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
leftX + (topX - leftX) * 0.25
} ${leftY - (leftY - topY) * 0.25}
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
} ${leftY - (leftY - topY) * 0.25}
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
C ${topX} ${topY}, ${topX} ${topY}, ${
topX + (rightX - topX) * 0.25
} ${topY + (rightY - topY) * 0.25}`,
@@ -608,7 +621,10 @@ const generateElementWithCanvas = (
if (
!prevElementWithCanvas ||
shouldRegenerateBecauseZoom ||
prevElementWithCanvas.theme !== renderConfig.theme
prevElementWithCanvas.theme !== renderConfig.theme ||
(renderConfig.editingImageElement &&
isImageElement(element) &&
element.id === renderConfig.editingImageElement.elementId)
) {
const elementWithCanvas = generateElementCanvas(
element,

View File

@@ -4,15 +4,13 @@ import {
NonDeleted,
} from "../element/types";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
type ElementKey = ExcalidrawElement | ExcalidrawElement["id"];
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
const isIdKey = (elementKey: ElementKey): elementKey is string => {
if (typeof elementKey === "string") {
return true;
}

View File

@@ -75,6 +75,7 @@ export const getElementContainingPosition = (
elements: readonly ExcalidrawElement[],
x: number,
y: number,
excludedType?: ExcalidrawElement["type"],
) => {
let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array)
@@ -83,7 +84,13 @@ export const getElementContainingPosition = (
continue;
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
if (x1 < x && x < x2 && y1 < y && y < y2) {
if (
x1 < x &&
x < x2 &&
y1 < y &&
y < y2 &&
elements[index].type !== excludedType
) {
hitElement = elements[index];
break;
}

View File

@@ -67,6 +67,7 @@ export const exportToCanvas = async (
renderSelection: false,
renderGrid: false,
isExporting: true,
editingImageElement: null,
});
return canvas;

View File

@@ -77,4 +77,4 @@ export const getTargetElements = (
) =>
appState.editingElement
? [appState.editingElement]
: getSelectedElements(elements, appState, true);
: getSelectedElements(elements, appState);

View File

@@ -11,6 +11,7 @@ export type RenderConfig = {
zoom: AppState["zoom"];
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
theme: AppState["theme"];
editingImageElement: AppState["editingImageElement"];
// collab-related state
// ---------------------------------------------------------------------------
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };

View File

@@ -29,6 +29,8 @@ import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { EditingImageElement } from "./element/imageEditor";
import Scene from "./scene/Scene";
export type Point = Readonly<RoughPoint>;
@@ -77,6 +79,7 @@ export type AppState = {
// (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
editingImageElement: EditingImageElement | null;
elementType: typeof SHAPES[number]["value"];
elementLocked: boolean;
exportBackground: boolean;
@@ -316,6 +319,8 @@ export type AppClassProperties = {
}
>;
files: BinaryFiles;
scene: Scene;
addFiles: App["addFiles"];
};
export type PointerDownState = Readonly<{

View File

@@ -443,7 +443,8 @@ export const bytesToHexString = (bytes: Uint8Array) => {
.join("");
};
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
export const getUpdatedTimestamp = () =>
process.env.NODE_ENV === "test" ? 1 : Date.now();
/**
* Transforms array of objects containing `id` attribute,
@@ -457,6 +458,3 @@ export const arrayToMap = <T extends { id: string } | string>(
return acc;
}, new Map());
};
export const isTestEnv = () =>
typeof process !== "undefined" && process.env?.NODE_ENV === "test";

141
yarn.lock
View File

@@ -2136,14 +2136,14 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@5.16.1":
version "5.16.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
"@testing-library/jest-dom@5.15.1":
version "5.15.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.15.1.tgz#4c49ba4d244f235aec53f0a83498daeb4ee06c33"
integrity sha512-kmj8opVDRE1E4GXyLlESsQthCXK7An28dFWxhiMwD7ZUI7ZxA6sjdJRxLerD9Jd8cHX4BDc1jzXaaZKqzlUkvg==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
aria-query "^4.2.2"
chalk "^3.0.0"
css "^3.0.0"
css.escape "^1.5.1"
@@ -2159,10 +2159,10 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
"@tldraw/vec@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.4.0.tgz#91a6d7c852c680a3288fe8bf79623252ef917281"
integrity sha512-YWkGe/IXdFB/Kts982fmtis2nJHB8KWVbkf0PlaZtQ1CL0TZKH0xdpIA/QJMk/WCj3bN4AOYuZKwEAegmWAnOA==
"@tldraw/vec@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.1.5.tgz#a0ec75742c20da43e3328f824ef7fca0f982b1fc"
integrity sha512-reEos3gJ9OiNvAYFtJzHbYWNjPTvWPmpjAY70HWHMjfhyNk6lHwwzDjwSgej4/KxVBbxB3ppNfLD9mEMq9yCOQ==
"@tootallnate/once@1":
version "1.1.2"
@@ -2219,10 +2219,10 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/chai@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
"@types/chai@4.2.22":
version "4.2.22"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7"
integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==
"@types/duplexify@^3.6.0":
version "3.6.0"
@@ -2338,10 +2338,10 @@
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/pako@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.3.tgz#2e61c2b02020b5f44e2e5e946dfac74f4ec33c58"
integrity sha512-EDxOsHAD5dqjbjEUM1xwa7rpKPFb8ECBE5irONTQU7/OsO3thI5YrNEWSPNMvYmvFM0l/OLQJ6Mgw7PEdXSjhg==
"@types/pako@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.2.tgz#17c9b136877f33d9ecc8e73cd26944f1f6dd39a1"
integrity sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==
"@types/parse-json@^4.0.0":
version "4.0.0"
@@ -2375,10 +2375,10 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@17.0.38":
version "17.0.38"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==
"@types/react@*", "@types/react@17.0.37":
version "17.0.37"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -4379,6 +4379,11 @@ clone@^1.0.2:
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clone@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
clsx@1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
@@ -5158,10 +5163,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
version "4.3.2"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
@@ -5718,7 +5723,7 @@ enhanced-resolve@^4.3.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enquirer@^2.3.5:
enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@@ -7750,11 +7755,6 @@ immer@8.0.1:
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
@@ -9276,23 +9276,24 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@12.1.4:
version "12.1.4"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.4.tgz#a92ec8509f13018caaafade61d515c2d5873316e"
integrity sha512-RgDz9nsFsE0/5eL9Vat0AvCuk0+j5mEuzBIVfrRH5FRtt5wibYe8zTjZs2nuqLFrLAGQGYnj8+HJxolcj08i/A==
lint-staged@12.1.2:
version "12.1.2"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.2.tgz#90c571927e1371fc133e720671dd7989eab53f74"
integrity sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==
dependencies:
cli-truncate "^3.1.0"
colorette "^2.0.16"
commander "^8.3.0"
debug "^4.3.3"
debug "^4.3.2"
enquirer "^2.3.6"
execa "^5.1.1"
lilconfig "2.0.4"
listr2 "^3.13.5"
listr2 "^3.13.3"
micromatch "^4.0.4"
normalize-path "^3.0.0"
object-inspect "^1.11.1"
object-inspect "^1.11.0"
string-argv "^0.3.1"
supports-color "^9.2.1"
supports-color "^9.0.2"
yaml "^1.10.2"
listenercount@~1.0.1:
@@ -9300,16 +9301,16 @@ listenercount@~1.0.1:
resolved "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz"
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
listr2@^3.13.5:
version "3.13.5"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.5.tgz#105a813f2eb2329c4aae27373a281d610ee4985f"
integrity sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==
listr2@^3.13.3:
version "3.13.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.3.tgz#d8f6095c9371b382c9b1c2bc33c5941d8e177f11"
integrity sha512-VqAgN+XVfyaEjSaFewGPcDs5/3hBbWVaX1VgWv2f52MF7US45JuARlArULctiB44IIcEk3JF7GtoFCLqEdeuPA==
dependencies:
cli-truncate "^2.1.0"
clone "^2.1.2"
colorette "^2.0.16"
log-update "^4.0.0"
p-map "^4.0.0"
rfdc "^1.3.0"
rxjs "^7.4.0"
through "^2.3.8"
wrap-ansi "^7.0.0"
@@ -10403,10 +10404,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-inspect@^1.11.1, object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
object-is@^1.0.1:
version "1.1.5"
@@ -11787,10 +11788,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
prettier@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893"
integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==
pretty-bytes@^5.3.0:
version "5.6.0"
@@ -12718,11 +12719,6 @@ rework@1.0.1:
convert-source-map "^0.3.3"
css "^2.0.0"
rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz"
@@ -12915,14 +12911,12 @@ sass-loader@^10.0.5:
schema-utils "^3.0.0"
semver "^7.3.2"
sass@1.45.2:
version "1.45.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.45.2.tgz#130b428c1692201cfa181139835d6fc378a33323"
integrity sha512-cKfs+F9AMPAFlbbTXNsbGvg3y58nV0mXA3E94jqaySKcC8Kq3/8983zVKQ0TLMUrHw7hF9Tnd3Bz9z5Xgtrl9g==
sass@1.43.5:
version "1.43.5"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.5.tgz#25a9d91dd098793ef7229d7b04dd3daae2fc4a65"
integrity sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@~1.2.4:
version "1.2.4"
@@ -13354,11 +13348,6 @@ source-list-map@^2.0.0:
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
@@ -13875,10 +13864,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
supports-color@^9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891"
integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==
supports-color@^9.0.2:
version "9.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.1.0.tgz#558963681dafeff41ed68220488cbf438d29f351"
integrity sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==
supports-hyperlinks@^1.0.1:
version "1.0.1"
@@ -14392,10 +14381,10 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
typescript@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.39"