mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-26 02:40:07 +02:00
Compare commits
5 Commits
frame-grou
...
dwelle/bum
Author | SHA1 | Date | |
---|---|---|---|
![]() |
72bc871b47 | ||
![]() |
9c425224c7 | ||
![]() |
9d1d45a8ea | ||
![]() |
029c3c48ba | ||
![]() |
adfd95be33 |
@@ -2888,9 +2888,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001366:
|
||||
version "1.0.30001370"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001370.tgz#0a30d4f20d38b9e108cc5ae7cc62df9fe66cd5ba"
|
||||
integrity sha512-3PDmaP56wz/qz7G508xzjx8C+MC2qEm4SYhSEzC9IBROo+dGXFWRuaXkWti0A9tuI00g+toiriVqxtWMgl350g==
|
||||
version "1.0.30001562"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001562.tgz"
|
||||
integrity sha512-kfte3Hym//51EdX4239i+Rmp20EsLIYGdPkERegTgU19hQWCRhsRFGKHTliUlsry53tv17K7n077Kqa0WJU4ng==
|
||||
|
||||
ccount@^1.0.0:
|
||||
version "1.1.0"
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
hasStrokeWidth,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
@@ -218,10 +218,12 @@ export const ShapesSwitcher = ({
|
||||
activeTool,
|
||||
appState,
|
||||
app,
|
||||
UIOptions,
|
||||
}: {
|
||||
activeTool: UIAppState["activeTool"];
|
||||
appState: UIAppState;
|
||||
app: AppClassProperties;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
|
||||
@@ -232,6 +234,14 @@ export const ShapesSwitcher = ({
|
||||
return (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
if (
|
||||
UIOptions.tools?.[
|
||||
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
|
||||
] === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter =
|
||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||
|
@@ -341,6 +341,7 @@ import {
|
||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { ImageSceneDataError } from "../errors";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
snapDraggedElements,
|
||||
@@ -2272,6 +2273,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||
if (!this.isToolSupported("image")) {
|
||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||
return;
|
||||
}
|
||||
|
||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||
this.insertImageElement(imageElement, file);
|
||||
this.initializeImageDimensions(imageElement);
|
||||
@@ -2477,7 +2483,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
mixedContent.some((node) => node.type === "imageUrl")
|
||||
mixedContent.some((node) => node.type === "imageUrl") &&
|
||||
this.isToolSupported("image")
|
||||
) {
|
||||
const imageURLs = mixedContent
|
||||
.filter((node) => node.type === "imageUrl")
|
||||
@@ -3284,6 +3291,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
// We purposely widen the `tool` type so this helper can be called with
|
||||
// any tool without having to type check it
|
||||
private isToolSupported = <T extends ToolType | "custom">(tool: T) => {
|
||||
return (
|
||||
this.props.UIOptions.tools?.[
|
||||
tool as Extract<T, keyof AppProps["UIOptions"]["tools"]>
|
||||
] !== false
|
||||
);
|
||||
};
|
||||
|
||||
setActiveTool = (
|
||||
tool: (
|
||||
| (
|
||||
@@ -3296,6 +3313,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) => {
|
||||
if (!this.isToolSupported(tool.type)) {
|
||||
console.warn(
|
||||
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||
if (nextActiveTool.type === "hand") {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
@@ -4740,9 +4764,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||
|
||||
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
mutateElement(pendingImageElement, {
|
||||
x,
|
||||
y,
|
||||
frameId: frame ? frame.id : null,
|
||||
});
|
||||
} else if (this.state.activeTool.type === "freedraw") {
|
||||
this.handleFreeDrawElementOnPointerDown(
|
||||
@@ -5609,9 +5637,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private createImageElement = ({
|
||||
sceneX,
|
||||
sceneY,
|
||||
addToFrameUnderCursor = true,
|
||||
}: {
|
||||
sceneX: number;
|
||||
sceneY: number;
|
||||
addToFrameUnderCursor?: boolean;
|
||||
}) => {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
sceneX,
|
||||
@@ -5621,10 +5651,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
});
|
||||
const topLayerFrame = addToFrameUnderCursor
|
||||
? this.getTopLayerFrameAtSceneCoords({
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
})
|
||||
: null;
|
||||
|
||||
const element = newImageElement({
|
||||
type: "image",
|
||||
@@ -6868,13 +6900,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
topLayerFrame &&
|
||||
!this.state.selectedElementIds[topLayerFrame.id]
|
||||
) {
|
||||
const processedGroupIds = new Map<string, boolean>();
|
||||
const elementsToAdd = selectedElements.filter(
|
||||
(element) =>
|
||||
element.frameId !== topLayerFrame.id &&
|
||||
isElementInFrame(element, nextElements, this.state, {
|
||||
processedGroupIds,
|
||||
}),
|
||||
isElementInFrame(element, nextElements, this.state),
|
||||
);
|
||||
|
||||
if (this.state.editingGroupId) {
|
||||
@@ -7474,6 +7503,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageFile: File,
|
||||
showCursorImagePreview?: boolean,
|
||||
) => {
|
||||
// we should be handling all cases upstream, but in case we forget to handle
|
||||
// a future case, let's throw here
|
||||
if (!this.isToolSupported("image")) {
|
||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.addNewElement(imageElement);
|
||||
|
||||
try {
|
||||
@@ -7557,6 +7593,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const imageElement = this.createImageElement({
|
||||
sceneX: x,
|
||||
sceneY: y,
|
||||
addToFrameUnderCursor: false,
|
||||
});
|
||||
|
||||
if (insertOnCanvasDirectly) {
|
||||
@@ -7857,7 +7894,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
try {
|
||||
if (isSupportedImageFile(file)) {
|
||||
// if image tool not supported, don't show an error here and let it fall
|
||||
// through so we still support importing scene data from images. If no
|
||||
// scene data encoded, we'll show an error then
|
||||
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
|
||||
// first attempt to decode scene from the image if it's embedded
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -7985,6 +8025,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof ImageSceneDataError &&
|
||||
error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
|
||||
!this.isToolSupported("image")
|
||||
) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMessage: t("errors.imageToolNotSupported"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({ isLoading: false, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
@@ -280,6 +280,7 @@ const LayerUI = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
@@ -470,6 +471,7 @@ const LayerUI = ({
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
)}
|
||||
{!device.editor.isMobile && (
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
@@ -45,6 +46,7 @@ type MobileMenuProps = {
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen: boolean;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
app: AppClassProperties;
|
||||
};
|
||||
|
||||
@@ -62,6 +64,7 @@ export const MobileMenu = ({
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
UIOptions,
|
||||
app,
|
||||
}: MobileMenuProps) => {
|
||||
const {
|
||||
@@ -83,6 +86,7 @@ export const MobileMenu = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
|
@@ -222,6 +222,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
toggleTheme: null,
|
||||
saveAsImage: true,
|
||||
},
|
||||
tools: {
|
||||
image: true,
|
||||
},
|
||||
};
|
||||
|
||||
// breakpoints
|
||||
|
@@ -99,7 +99,7 @@ export const setCursorForShape = (
|
||||
interactiveCanvas.style.cursor = `url(${url}), auto`;
|
||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||
} else {
|
||||
} else if (appState.activeTool.type !== "image") {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
|
||||
}
|
||||
};
|
||||
|
@@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
|
||||
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
import { ExcalidrawElement, FileId } from "../element/types";
|
||||
import { CanvasError } from "../errors";
|
||||
import { CanvasError, ImageSceneDataError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { AppState, DataURL, LibraryItem } from "../types";
|
||||
@@ -24,15 +24,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
||||
).decodePngMetadata(blob);
|
||||
} catch (error: any) {
|
||||
if (error.message === "INVALID") {
|
||||
throw new DOMException(
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"EncodingError",
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
} else {
|
||||
throw new DOMException(
|
||||
t("alerts.cannotRestoreFromImage"),
|
||||
"EncodingError",
|
||||
);
|
||||
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -58,15 +55,12 @@ const parseFileContents = async (blob: Blob | File) => {
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message === "INVALID") {
|
||||
throw new DOMException(
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"EncodingError",
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
} else {
|
||||
throw new DOMException(
|
||||
t("alerts.cannotRestoreFromImage"),
|
||||
"EncodingError",
|
||||
);
|
||||
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,8 +125,19 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
fileHandle?: FileSystemHandle | null,
|
||||
) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
let data;
|
||||
try {
|
||||
const data = JSON.parse(contents);
|
||||
try {
|
||||
data = JSON.parse(contents);
|
||||
} catch (error: any) {
|
||||
if (isSupportedImageFile(blob)) {
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (isValidExcalidrawData(data)) {
|
||||
return {
|
||||
type: MIME_TYPES.excalidraw,
|
||||
@@ -162,7 +167,9 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
if (error instanceof ImageSceneDataError) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
};
|
||||
|
@@ -16,3 +16,19 @@ export class AbortError extends DOMException {
|
||||
super(message, "AbortError");
|
||||
}
|
||||
}
|
||||
|
||||
type ImageSceneDataErrorCode =
|
||||
| "IMAGE_NOT_CONTAINS_SCENE_DATA"
|
||||
| "IMAGE_SCENE_DATA_ERROR";
|
||||
|
||||
export class ImageSceneDataError extends Error {
|
||||
public code;
|
||||
constructor(
|
||||
message = "Image Scene Data Error",
|
||||
code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "EncodingError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
134
src/frame.ts
134
src/frame.ts
@@ -1,4 +1,8 @@
|
||||
import { getCommonBounds, getElementBounds, isTextElement } from "./element";
|
||||
import {
|
||||
getCommonBounds,
|
||||
getElementAbsoluteCoords,
|
||||
isTextElement,
|
||||
} from "./element";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameElement,
|
||||
@@ -52,7 +56,6 @@ export const bindElementsToFramesAfterDuplication = (
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------- Frame Geometry ---------------------------------
|
||||
export function isElementIntersectingFrame(
|
||||
element: ExcalidrawElement,
|
||||
frame: ExcalidrawFrameElement,
|
||||
@@ -82,27 +85,36 @@ export const getElementsCompletelyInFrame = (
|
||||
element.frameId === frame.id,
|
||||
);
|
||||
|
||||
export const isElementContainingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
element: ExcalidrawElement,
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => {
|
||||
return getElementsWithinSelection(elements, element).some(
|
||||
(e) => e.id === frame.id,
|
||||
);
|
||||
};
|
||||
|
||||
export const getElementsIntersectingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
||||
|
||||
export const elementsAreInBounds = (
|
||||
export const elementsAreInFrameBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
element: ExcalidrawElement,
|
||||
tolerance = 0,
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => {
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getElementBounds(element);
|
||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||
getElementAbsoluteCoords(frame);
|
||||
|
||||
const [elementsX1, elementsY1, elementsX2, elementsY2] =
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(elements);
|
||||
|
||||
return (
|
||||
elementX1 <= elementsX1 - tolerance &&
|
||||
elementY1 <= elementsY1 - tolerance &&
|
||||
elementX2 >= elementsX2 + tolerance &&
|
||||
elementY2 >= elementsY2 + tolerance
|
||||
selectionX1 <= elementX1 &&
|
||||
selectionY1 <= elementY1 &&
|
||||
selectionX2 >= elementX2 &&
|
||||
selectionY2 >= elementY2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,12 +123,9 @@ export const elementOverlapsWithFrame = (
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => {
|
||||
return (
|
||||
// frame contains element
|
||||
elementsAreInBounds([element], frame) ||
|
||||
// element contains frame
|
||||
(elementsAreInBounds([frame], element) && element.frameId === frame.id) ||
|
||||
// element intersects with frame
|
||||
isElementIntersectingFrame(element, frame)
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
isElementIntersectingFrame(element, frame) ||
|
||||
isElementContainingFrame([frame], element, frame)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -127,7 +136,7 @@ export const isCursorInFrame = (
|
||||
},
|
||||
frame: NonDeleted<ExcalidrawFrameElement>,
|
||||
) => {
|
||||
const [fx1, fy1, fx2, fy2] = getElementBounds(frame);
|
||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
|
||||
|
||||
return isPointWithinBounds(
|
||||
[fx1, fy1],
|
||||
@@ -151,7 +160,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
||||
|
||||
return !!elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInBounds([element], frame) ||
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
isElementIntersectingFrame(element, frame),
|
||||
);
|
||||
};
|
||||
@@ -172,7 +181,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
||||
return (
|
||||
elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInBounds([element], frame) ||
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
isElementIntersectingFrame(element, frame),
|
||||
) === undefined
|
||||
);
|
||||
@@ -240,18 +249,12 @@ export const getElementsInResizingFrame = (
|
||||
const prevElementsInFrame = getFrameChildren(allElements, frame.id);
|
||||
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
|
||||
|
||||
const elementsCompletelyInFrame = new Set<ExcalidrawElement>(
|
||||
getElementsCompletelyInFrame(allElements, frame),
|
||||
);
|
||||
|
||||
for (const element of prevElementsInFrame) {
|
||||
if (!elementsCompletelyInFrame.has(element)) {
|
||||
// element contains the frame
|
||||
if (elementsAreInBounds([frame], element)) {
|
||||
elementsCompletelyInFrame.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
const elementsCompletelyInFrame = new Set([
|
||||
...getElementsCompletelyInFrame(allElements, frame),
|
||||
...prevElementsInFrame.filter((element) =>
|
||||
isElementContainingFrame(allElements, element, frame),
|
||||
),
|
||||
]);
|
||||
|
||||
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
|
||||
(element) => !elementsCompletelyInFrame.has(element),
|
||||
@@ -318,7 +321,7 @@ export const getElementsInResizingFrame = (
|
||||
if (isSelected) {
|
||||
const elementsInGroup = getElementsInGroup(allElements, id);
|
||||
|
||||
if (elementsAreInBounds(elementsInGroup, frame)) {
|
||||
if (elementsAreInFrameBounds(elementsInGroup, frame)) {
|
||||
for (const element of elementsInGroup) {
|
||||
nextElementsInFrame.add(element);
|
||||
}
|
||||
@@ -367,7 +370,7 @@ export const getContainingFrame = (
|
||||
// --------------------------- Frame Operations -------------------------------
|
||||
|
||||
/**
|
||||
* Retains (or repairs for target frame) the ordering invariant where children
|
||||
* Retains (or repairs for target frame) the ordering invriant where children
|
||||
* elements come right before the parent frame:
|
||||
* [el, el, child, child, frame, el]
|
||||
*/
|
||||
@@ -434,14 +437,25 @@ export const removeElementsFromFrame = (
|
||||
ExcalidrawElement
|
||||
>();
|
||||
|
||||
const toRemoveElementsByFrame = new Map<
|
||||
ExcalidrawFrameElement["id"],
|
||||
ExcalidrawElement[]
|
||||
>();
|
||||
|
||||
for (const element of elementsToRemove) {
|
||||
if (element.frameId) {
|
||||
_elementsToRemove.set(element.id, element);
|
||||
|
||||
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
|
||||
arr.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||
arr.push(boundTextElement);
|
||||
}
|
||||
|
||||
toRemoveElementsByFrame.set(element.frameId, arr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,15 +520,12 @@ export const updateFrameMembershipOfSelectedElements = (
|
||||
}
|
||||
|
||||
const elementsToRemove = new Set<ExcalidrawElement>();
|
||||
const processedGroupIds = new Map<string, boolean>();
|
||||
|
||||
elementsToFilter.forEach((element) => {
|
||||
if (
|
||||
element.frameId &&
|
||||
!isFrameElement(element) &&
|
||||
!isElementInFrame(element, allElements, appState, {
|
||||
processedGroupIds,
|
||||
})
|
||||
!isElementInFrame(element, allElements, appState)
|
||||
) {
|
||||
elementsToRemove.add(element);
|
||||
}
|
||||
@@ -576,36 +587,26 @@ export const getTargetFrame = (
|
||||
: getContainingFrame(_element);
|
||||
};
|
||||
|
||||
// TODO: this a huge bottleneck for large scenes, optimise
|
||||
// given an element, return if the element is in some frame
|
||||
export const isElementInFrame = (
|
||||
element: ExcalidrawElement,
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
appState: StaticCanvasAppState,
|
||||
opts?: {
|
||||
targetFrame?: ExcalidrawFrameElement;
|
||||
processedGroupIds?: Map<string, boolean>;
|
||||
},
|
||||
) => {
|
||||
const frame = opts?.targetFrame ?? getTargetFrame(element, appState);
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
: element;
|
||||
|
||||
const groupsInFrame = (yes: boolean) => {
|
||||
if (opts?.processedGroupIds) {
|
||||
_element.groupIds.forEach((gid) => {
|
||||
opts.processedGroupIds?.set(gid, yes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (frame) {
|
||||
// Perf improvement:
|
||||
// For an element that's already in a frame, if it's not being selected
|
||||
// and its frame is not being selected, it has to be in its containing frame.
|
||||
// For an element that's already in a frame, if it's not being dragged
|
||||
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
|
||||
// It has to be in its containing frame.
|
||||
if (
|
||||
!appState.selectedElementIds[element.id] &&
|
||||
!appState.selectedElementIds[frame.id]
|
||||
!appState.selectedElementIds[element.id] ||
|
||||
!appState.selectedElementsAreBeingDragged
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -614,21 +615,8 @@ export const isElementInFrame = (
|
||||
return elementOverlapsWithFrame(_element, frame);
|
||||
}
|
||||
|
||||
for (const gid of _element.groupIds) {
|
||||
if (opts?.processedGroupIds?.has(gid)) {
|
||||
return opts.processedGroupIds.get(gid);
|
||||
}
|
||||
}
|
||||
|
||||
const allElementsInGroup = new Set(
|
||||
_element.groupIds
|
||||
.filter((gid) => {
|
||||
if (opts?.processedGroupIds) {
|
||||
return !opts.processedGroupIds.has(gid);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
||||
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
||||
);
|
||||
|
||||
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
||||
@@ -649,22 +637,16 @@ export const isElementInFrame = (
|
||||
|
||||
for (const elementInGroup of allElementsInGroup) {
|
||||
if (isFrameElement(elementInGroup)) {
|
||||
groupsInFrame(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const elementInGroup of allElementsInGroup) {
|
||||
if (elementOverlapsWithFrame(elementInGroup, frame)) {
|
||||
groupsInFrame(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_element.groupIds.length > 0) {
|
||||
groupsInFrame(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
@@ -232,8 +232,6 @@ export const selectGroupsFromGivenElements = (
|
||||
selectedGroupIds: {},
|
||||
};
|
||||
|
||||
const processedGroupIds = new Set<string>();
|
||||
|
||||
for (const element of elements) {
|
||||
let groupIds = element.groupIds;
|
||||
if (appState.editingGroupId) {
|
||||
@@ -244,13 +242,10 @@ export const selectGroupsFromGivenElements = (
|
||||
}
|
||||
if (groupIds.length > 0) {
|
||||
const groupId = groupIds[groupIds.length - 1];
|
||||
if (!processedGroupIds.has(groupId)) {
|
||||
nextAppState = {
|
||||
...nextAppState,
|
||||
...selectGroup(groupId, nextAppState, elements),
|
||||
};
|
||||
processedGroupIds.add(groupId);
|
||||
}
|
||||
nextAppState = {
|
||||
...nextAppState,
|
||||
...selectGroup(groupId, nextAppState, elements),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -209,6 +209,7 @@
|
||||
"importLibraryError": "Couldn't load library",
|
||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||
"imageToolNotSupported": "Images are disabled.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
|
||||
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||
|
@@ -15,28 +15,54 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for disabling `image` tool (also disabling image insertion in general, though keeps support for importing from `.excalidraw` files) [#6320](https://github.com/excalidraw/excalidraw/pull/6320).
|
||||
|
||||
For disabling `image` you need to set 👇
|
||||
|
||||
```
|
||||
UIOptions.tools = {
|
||||
image: false
|
||||
}
|
||||
```
|
||||
|
||||
- Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api)
|
||||
|
||||
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0.
|
||||
|
||||
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
|
||||
|
||||
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205).
|
||||
|
||||
- Export `elementsOverlappingBBox`, `isElementInsideBBox`, `elementPartiallyOverlapsWithOrContainsBBox` helpers for filtering/checking if elements within bounds. [#6727](https://github.com/excalidraw/excalidraw/pull/6727)
|
||||
|
||||
- Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195)
|
||||
|
||||
- Add onChange, onPointerDown, onPointerUp api subscribers [#7154](https://github.com/excalidraw/excalidraw/pull/7154).
|
||||
|
||||
- Support props.locked for setActiveTool [#7153](https://github.com/excalidraw/excalidraw/pull/7153).
|
||||
|
||||
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078)
|
||||
|
||||
#### BREAKING CHANGES
|
||||
### Fixes
|
||||
|
||||
- Double image dialog on image insertion [#7152](https://github.com/excalidraw/excalidraw/pull/7152).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api) [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0 [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
- [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243)
|
||||
|
||||
### Build
|
||||
|
||||
- Support Preact [#7255](https://github.com/excalidraw/excalidraw/pull/7255). The host needs to set `process.env.IS_PREACT` to `true`
|
||||
|
||||
When using vite, you will have to make sure the variable process.env.IS_PREACT is available at runtime since Vite removes it by default, so you can update the vite config to ensure its available
|
||||
|
||||
```js
|
||||
define: {
|
||||
"process.env.IS_PREACT": process.env.IS_PREACT,
|
||||
}
|
||||
```
|
||||
|
||||
## 0.16.1 (2023-09-21)
|
||||
|
||||
## Excalidraw Library
|
||||
|
@@ -98,6 +98,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [disableImageTool, setDisableImageTool] = useState(false);
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
@@ -606,6 +607,16 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableImageTool === true}
|
||||
onChange={() => {
|
||||
setDisableImageTool(!disableImageTool);
|
||||
}}
|
||||
/>
|
||||
Disable Image Tool
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -686,6 +697,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
|
@@ -56,6 +56,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
tools: {
|
||||
image: props.UIOptions?.tools?.image ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
if (canvasActions?.export) {
|
||||
|
@@ -1,4 +1,10 @@
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.IS_PREACT === "true") {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
module.exports = require("./dist/excalidraw-with-preact.production.min.js");
|
||||
} else {
|
||||
module.exports = require("./dist/excalidraw-with-preact.development.js");
|
||||
}
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
module.exports = require("./dist/excalidraw.production.min.js");
|
||||
} else {
|
||||
module.exports = require("./dist/excalidraw.development.js");
|
||||
|
@@ -78,7 +78,7 @@
|
||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
|
||||
"scripts": {
|
||||
"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": "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 && NODE_ENV=development webpack --config webpack.preact.config.js && NODE_ENV=production webpack --config webpack.preact.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",
|
||||
|
33
src/packages/excalidraw/webpack.preact.config.js
Normal file
33
src/packages/excalidraw/webpack.preact.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const prodConfig = require("./webpack.prod.config");
|
||||
const devConfig = require("./webpack.dev.config");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
const config = isProd ? prodConfig : devConfig;
|
||||
const outputFile = isProd
|
||||
? "excalidraw-with-preact.production.min"
|
||||
: "excalidraw-with-preact.development";
|
||||
|
||||
const preactWebpackConfig = {
|
||||
entry: {
|
||||
[outputFile]: "./entry.js",
|
||||
},
|
||||
externals: {
|
||||
...config.externals,
|
||||
"react-dom/client": {
|
||||
root: "ReactDOMClient",
|
||||
commonjs2: "react-dom/client",
|
||||
commonjs: "react-dom/client",
|
||||
amd: "react-dom/client",
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
root: "ReactJSXRuntime",
|
||||
commonjs2: "react/jsx-runtime",
|
||||
commonjs: "react/jsx-runtime",
|
||||
amd: "react/jsx-runtime",
|
||||
},
|
||||
},
|
||||
};
|
||||
module.exports = merge(config, preactWebpackConfig);
|
@@ -71,7 +71,6 @@ import { renderSnaps } from "./renderSnaps";
|
||||
import {
|
||||
isEmbeddableElement,
|
||||
isFrameElement,
|
||||
isFreeDrawElement,
|
||||
isLinearElement,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
@@ -79,7 +78,7 @@ import {
|
||||
createPlaceholderEmbeddableLabel,
|
||||
} from "../element/embeddable";
|
||||
import {
|
||||
elementsAreInBounds,
|
||||
elementOverlapsWithFrame,
|
||||
getTargetFrame,
|
||||
isElementInFrame,
|
||||
} from "../frame";
|
||||
@@ -946,79 +945,102 @@ const _renderStaticScene = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Paint visible elements with embeddables on top
|
||||
const visibleNonEmbeddableOrLabelElements = visibleElements.filter(
|
||||
(el) => !isEmbeddableOrLabel(el),
|
||||
);
|
||||
|
||||
const visibleEmbeddableOrLabelElements = visibleElements.filter((el) =>
|
||||
isEmbeddableOrLabel(el),
|
||||
);
|
||||
|
||||
const visibleElementsToRender = [
|
||||
...visibleNonEmbeddableOrLabelElements,
|
||||
...visibleEmbeddableOrLabelElements,
|
||||
];
|
||||
|
||||
const _renderElement = (element: ExcalidrawElement) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
|
||||
if (
|
||||
isEmbeddableElement(element) &&
|
||||
(isExporting || !element.validated) &&
|
||||
element.width &&
|
||||
element.height
|
||||
) {
|
||||
const label = createPlaceholderEmbeddableLabel(element);
|
||||
renderElement(label, rc, context, renderConfig, appState);
|
||||
}
|
||||
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const processedGroupIds = new Map<string, boolean>();
|
||||
for (const element of visibleElementsToRender) {
|
||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||
const groupsToBeAddedToFrame = new Set<string>();
|
||||
|
||||
visibleElements.forEach((element) => {
|
||||
if (
|
||||
frameId &&
|
||||
appState.frameRendering.enabled &&
|
||||
appState.frameRendering.clip
|
||||
element.groupIds.length > 0 &&
|
||||
appState.frameToHighlight &&
|
||||
appState.selectedElementIds[element.id] &&
|
||||
(elementOverlapsWithFrame(element, appState.frameToHighlight) ||
|
||||
element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
|
||||
) {
|
||||
const targetFrame = getTargetFrame(element, appState);
|
||||
// for perf:
|
||||
// only clip elements that are not completely in the target frame
|
||||
if (
|
||||
targetFrame &&
|
||||
!elementsAreInBounds(
|
||||
[element],
|
||||
targetFrame,
|
||||
isFreeDrawElement(element)
|
||||
? element.strokeWidth * 8
|
||||
: element.roughness * (isLinearElement(element) ? 8 : 4),
|
||||
) &&
|
||||
isElementInFrame(element, elements, appState, {
|
||||
targetFrame,
|
||||
processedGroupIds,
|
||||
})
|
||||
) {
|
||||
context.save();
|
||||
frameClip(targetFrame, context, renderConfig, appState);
|
||||
_renderElement(element);
|
||||
context.restore();
|
||||
} else {
|
||||
_renderElement(element);
|
||||
}
|
||||
} else {
|
||||
_renderElement(element);
|
||||
element.groupIds.forEach((groupId) =>
|
||||
groupsToBeAddedToFrame.add(groupId),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Paint visible elements
|
||||
visibleElements
|
||||
.filter((el) => !isEmbeddableOrLabel(el))
|
||||
.forEach((element) => {
|
||||
try {
|
||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||
|
||||
if (
|
||||
frameId &&
|
||||
appState.frameRendering.enabled &&
|
||||
appState.frameRendering.clip
|
||||
) {
|
||||
context.save();
|
||||
|
||||
const frame = getTargetFrame(element, appState);
|
||||
|
||||
// TODO do we need to check isElementInFrame here?
|
||||
if (frame && isElementInFrame(element, elements, appState)) {
|
||||
frameClip(frame, context, renderConfig, appState);
|
||||
}
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
context.restore();
|
||||
} else {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
}
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
// render embeddables on top
|
||||
visibleElements
|
||||
.filter((el) => isEmbeddableOrLabel(el))
|
||||
.forEach((element) => {
|
||||
try {
|
||||
const render = () => {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
|
||||
if (
|
||||
isEmbeddableElement(element) &&
|
||||
(isExporting || !element.validated) &&
|
||||
element.width &&
|
||||
element.height
|
||||
) {
|
||||
const label = createPlaceholderEmbeddableLabel(element);
|
||||
renderElement(label, rc, context, renderConfig, appState);
|
||||
}
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
};
|
||||
// - when exporting the whole canvas, we DO NOT apply clipping
|
||||
// - when we are exporting a particular frame, apply clipping
|
||||
// if the containing frame is not selected, apply clipping
|
||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||
|
||||
if (
|
||||
frameId &&
|
||||
appState.frameRendering.enabled &&
|
||||
appState.frameRendering.clip
|
||||
) {
|
||||
context.save();
|
||||
|
||||
const frame = getTargetFrame(element, appState);
|
||||
|
||||
if (frame && isElementInFrame(element, elements, appState)) {
|
||||
frameClip(frame, context, renderConfig, appState);
|
||||
}
|
||||
render();
|
||||
context.restore();
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** throttled to animation framerate */
|
||||
@@ -1123,7 +1145,7 @@ const renderTransformHandles = (
|
||||
|
||||
const renderSelectionBorder = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState | StaticCanvasAppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
elementProperties: {
|
||||
angle: number;
|
||||
elementX1: number;
|
||||
@@ -1288,23 +1310,6 @@ const renderFrameHighlight = (
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(elements);
|
||||
return {
|
||||
angle: 0,
|
||||
elementX1,
|
||||
elementX2,
|
||||
elementY1,
|
||||
elementY2,
|
||||
selectionColors: ["rgb(0,118,255)"],
|
||||
dashed: false,
|
||||
cx: elementX1 + (elementX2 - elementX1) / 2,
|
||||
cy: elementY1 + (elementY2 - elementY1) / 2,
|
||||
activeEmbeddable: false,
|
||||
};
|
||||
};
|
||||
|
||||
const renderElementsBoxHighlight = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
@@ -1318,28 +1323,37 @@ const renderElementsBoxHighlight = (
|
||||
(element) => element.groupIds.length > 0,
|
||||
);
|
||||
|
||||
const processedGroupIds = new Set<string>();
|
||||
const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(elements);
|
||||
return {
|
||||
angle: 0,
|
||||
elementX1,
|
||||
elementX2,
|
||||
elementY1,
|
||||
elementY2,
|
||||
selectionColors: ["rgb(0,118,255)"],
|
||||
dashed: false,
|
||||
cx: elementX1 + (elementX2 - elementX1) / 2,
|
||||
cy: elementY1 + (elementY2 - elementY1) / 2,
|
||||
activeEmbeddable: false,
|
||||
};
|
||||
};
|
||||
|
||||
const getSelectionForGroupId = (groupId: GroupId) => {
|
||||
if (!processedGroupIds.has(groupId)) {
|
||||
const groupElements = getElementsInGroup(elements, groupId);
|
||||
processedGroupIds.add(groupId);
|
||||
return getSelectionFromElements(groupElements);
|
||||
}
|
||||
|
||||
return null;
|
||||
const groupElements = getElementsInGroup(elements, groupId);
|
||||
return getSelectionFromElements(groupElements);
|
||||
};
|
||||
|
||||
Object.entries(selectGroupsFromGivenElements(elementsInGroups, appState))
|
||||
.filter(([id, isSelected]) => isSelected)
|
||||
.map(([id, isSelected]) => id)
|
||||
.map((groupId) => getSelectionForGroupId(groupId))
|
||||
.filter((selection) => selection)
|
||||
.concat(
|
||||
individualElements.map((element) => getSelectionFromElements([element])),
|
||||
)
|
||||
.forEach((selection) =>
|
||||
renderSelectionBorder(context, appState, selection!),
|
||||
renderSelectionBorder(context, appState, selection),
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -471,7 +471,7 @@ export type ExportOpts = {
|
||||
// truthiness value will determine whether the action is rendered or not
|
||||
// (see manager renderAction). We also override canvasAction values in
|
||||
// excalidraw package index.tsx.
|
||||
type CanvasActions = Partial<{
|
||||
export type CanvasActions = Partial<{
|
||||
changeViewBackgroundColor: boolean;
|
||||
clearCanvas: boolean;
|
||||
export: false | ExportOpts;
|
||||
@@ -481,9 +481,12 @@ type CanvasActions = Partial<{
|
||||
saveAsImage: boolean;
|
||||
}>;
|
||||
|
||||
type UIOptions = Partial<{
|
||||
export type UIOptions = Partial<{
|
||||
dockedSidebarBreakpoint: number;
|
||||
canvasActions: CanvasActions;
|
||||
tools: {
|
||||
image: boolean;
|
||||
};
|
||||
/** @deprecated does nothing. Will be removed in 0.15 */
|
||||
welcomeScreen?: boolean;
|
||||
}>;
|
||||
|
Reference in New Issue
Block a user