Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e43bf7c7 | ||
|
|
9d24003517 |
@@ -21,8 +21,10 @@ import type { AppClassProperties, AppState, UIAppState } from "../types";
|
|||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const alignActionsPredicate = (
|
const alignActionsPredicate = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
|
_: unknown,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
@@ -46,7 +48,6 @@ const alignSelectedElements = (
|
|||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
alignment,
|
alignment,
|
||||||
app.scene,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
@@ -63,8 +64,7 @@ export const actionAlignTop = register({
|
|||||||
label: "labels.alignTop",
|
label: "labels.alignTop",
|
||||||
icon: AlignTopIcon,
|
icon: AlignTopIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -79,7 +79,7 @@ export const actionAlignTop = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -97,8 +97,7 @@ export const actionAlignBottom = register({
|
|||||||
label: "labels.alignBottom",
|
label: "labels.alignBottom",
|
||||||
icon: AlignBottomIcon,
|
icon: AlignBottomIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -113,7 +112,7 @@ export const actionAlignBottom = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -131,8 +130,7 @@ export const actionAlignLeft = register({
|
|||||||
label: "labels.alignLeft",
|
label: "labels.alignLeft",
|
||||||
icon: AlignLeftIcon,
|
icon: AlignLeftIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -147,7 +145,7 @@ export const actionAlignLeft = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -165,8 +163,7 @@ export const actionAlignRight = register({
|
|||||||
label: "labels.alignRight",
|
label: "labels.alignRight",
|
||||||
icon: AlignRightIcon,
|
icon: AlignRightIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -181,7 +178,7 @@ export const actionAlignRight = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -199,8 +196,7 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
label: "labels.centerVertically",
|
label: "labels.centerVertically",
|
||||||
icon: CenterVerticallyIcon,
|
icon: CenterVerticallyIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -213,7 +209,7 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -229,8 +225,7 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
label: "labels.centerHorizontally",
|
label: "labels.centerHorizontally",
|
||||||
icon: CenterHorizontallyIcon,
|
icon: CenterHorizontallyIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, appProps, app) =>
|
predicate: alignActionsPredicate,
|
||||||
alignActionsPredicate(appState, app),
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -243,7 +238,7 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterHorizontallyIcon}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { frameToolIcon } from "../components/icons";
|
|||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { newFrameElement } from "../element/newElement";
|
import { newFrameElement } from "../element/newElement";
|
||||||
import { getElementsInGroup } from "../groups";
|
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
|
|
||||||
const isSingleFrameSelected = (
|
const isSingleFrameSelected = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@@ -176,31 +174,10 @@ export const actionWrapSelectionInFrame = register({
|
|||||||
height: y2 - y1 + PADDING * 2,
|
height: y2 - y1 + PADDING * 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// for a selected partial group, we want to remove it from the remainder of the group
|
|
||||||
if (appState.editingGroupId) {
|
|
||||||
const elementsInGroup = getElementsInGroup(
|
|
||||||
selectedElements,
|
|
||||||
appState.editingGroupId,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const elementInGroup of elementsInGroup) {
|
|
||||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
elementInGroup,
|
|
||||||
{
|
|
||||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextElements = addElementsToFrame(
|
const nextElements = addElementsToFrame(
|
||||||
[...app.scene.getElementsIncludingDeleted(), frame],
|
[...app.scene.getElementsIncludingDeleted(), frame],
|
||||||
selectedElements,
|
selectedElements,
|
||||||
frame,
|
frame,
|
||||||
appState,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ import type {
|
|||||||
import type { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
frameAndChildrenSelectedTogether,
|
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
getFrameLikeElements,
|
getFrameLikeElements,
|
||||||
getRootElements,
|
|
||||||
groupByFrameLikes,
|
groupByFrameLikes,
|
||||||
removeElementsFromFrame,
|
removeElementsFromFrame,
|
||||||
replaceAllElementsInFrame,
|
replaceAllElementsInFrame,
|
||||||
@@ -62,11 +60,8 @@ const enableActionGroup = (
|
|||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 &&
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
!allElementsInSameGroup(selectedElements) &&
|
|
||||||
!frameAndChildrenSelectedTogether(selectedElements)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,12 +71,10 @@ export const actionGroup = register({
|
|||||||
icon: (appState) => <GroupIcon theme={appState.theme} />,
|
icon: (appState) => <GroupIcon theme={appState.theme} />,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getRootElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
app.scene.getSelectedElements({
|
selectedElementIds: appState.selectedElementIds,
|
||||||
selectedElementIds: appState.selectedElementIds,
|
includeBoundTextElement: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (selectedElements.length < 2) {
|
if (selectedElements.length < 2) {
|
||||||
// nothing to group
|
// nothing to group
|
||||||
return { appState, elements, storeAction: StoreAction.NONE };
|
return { appState, elements, storeAction: StoreAction.NONE };
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ import type {
|
|||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -116,7 +115,6 @@ import {
|
|||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
calculateFixedPointForElbowArrowBinding,
|
calculateFixedPointForElbowArrowBinding,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
updateBoundElements,
|
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
@@ -220,47 +218,33 @@ const changeFontSize = (
|
|||||||
) => {
|
) => {
|
||||||
const newFontSizes = new Set<number>();
|
const newFontSizes = new Set<number>();
|
||||||
|
|
||||||
const updatedElements = changeProperty(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
(oldElement) => {
|
|
||||||
if (isTextElement(oldElement)) {
|
|
||||||
const newFontSize = getNewFontSize(oldElement);
|
|
||||||
newFontSizes.add(newFontSize);
|
|
||||||
|
|
||||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
|
||||||
fontSize: newFontSize,
|
|
||||||
});
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
|
||||||
app.scene.getContainerElement(oldElement),
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
|
||||||
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
return oldElement;
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update arrow elements after text elements have been updated
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
|
||||||
getSelectedElements(elements, appState, {
|
|
||||||
includeBoundTextElement: true,
|
|
||||||
}).forEach((element) => {
|
|
||||||
if (isTextElement(element)) {
|
|
||||||
updateBoundElements(
|
|
||||||
element,
|
|
||||||
updatedElementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: updatedElements,
|
elements: changeProperty(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(oldElement) => {
|
||||||
|
if (isTextElement(oldElement)) {
|
||||||
|
const newFontSize = getNewFontSize(oldElement);
|
||||||
|
newFontSizes.add(newFontSize);
|
||||||
|
|
||||||
|
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||||
|
fontSize: newFontSize,
|
||||||
|
});
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||||
|
|
||||||
|
return newElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldElement;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
// update state only if we've set all select text elements to
|
// update state only if we've set all select text elements to
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getNonDeletedElements, isTextElement } from "../element";
|
|||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
import { selectAllIcon } from "../components/icons";
|
import { selectAllIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|
||||||
@@ -19,17 +20,17 @@ export const actionSelectAll = register({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElementIds = elements
|
const selectedElementIds = excludeElementsInFramesFromSelection(
|
||||||
.filter(
|
elements.filter(
|
||||||
(element) =>
|
(element) =>
|
||||||
!element.isDeleted &&
|
!element.isDeleted &&
|
||||||
!(isTextElement(element) && element.containerId) &&
|
!(isTextElement(element) && element.containerId) &&
|
||||||
!element.locked,
|
!element.locked,
|
||||||
)
|
),
|
||||||
.reduce((map: Record<ExcalidrawElement["id"], true>, element) => {
|
).reduce((map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
map[element.id] = true;
|
map[element.id] = true;
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import type { BoundingBox } from "./element/bounds";
|
import type { BoundingBox } from "./element/bounds";
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
import { getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
import { updateBoundElements } from "./element/binding";
|
|
||||||
import type Scene from "./scene/Scene";
|
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
@@ -15,7 +13,6 @@ export const alignElements = (
|
|||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
scene: Scene,
|
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
@@ -29,18 +26,12 @@ export const alignElements = (
|
|||||||
selectionBoundingBox,
|
selectionBoundingBox,
|
||||||
alignment,
|
alignment,
|
||||||
);
|
);
|
||||||
return group.map((element) => {
|
return group.map((element) =>
|
||||||
// update element
|
newElementWith(element, {
|
||||||
const updatedEle = mutateElement(element, {
|
|
||||||
x: element.x + translation.x,
|
x: element.x + translation.x,
|
||||||
y: element.y + translation.y,
|
y: element.y + translation.y,
|
||||||
});
|
}),
|
||||||
// update bound elements
|
);
|
||||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
|
||||||
simultaneouslyUpdated: group,
|
|
||||||
});
|
|
||||||
return updatedEle;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface AnimatedTrailOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AnimatedTrail implements Trail {
|
export class AnimatedTrail implements Trail {
|
||||||
currentTrail?: LaserPointer;
|
private currentTrail?: LaserPointer;
|
||||||
private pastTrails: LaserPointer[] = [];
|
private pastTrails: LaserPointer[] = [];
|
||||||
|
|
||||||
private container?: SVGSVGElement;
|
private container?: SVGSVGElement;
|
||||||
@@ -28,7 +28,7 @@ export class AnimatedTrail implements Trail {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private animationFrameHandler: AnimationFrameHandler,
|
private animationFrameHandler: AnimationFrameHandler,
|
||||||
protected app: App,
|
private app: App,
|
||||||
private options: Partial<LaserPointerOptions> &
|
private options: Partial<LaserPointerOptions> &
|
||||||
Partial<AnimatedTrailOptions>,
|
Partial<AnimatedTrailOptions>,
|
||||||
) {
|
) {
|
||||||
@@ -98,16 +98,6 @@ export class AnimatedTrail implements Trail {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentTrail() {
|
|
||||||
return this.currentTrail;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTrails() {
|
|
||||||
this.pastTrails = [];
|
|
||||||
this.currentTrail = undefined;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
isCropping: false,
|
isCropping: false,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
searchMatches: [],
|
searchMatches: [],
|
||||||
lassoSelectionEnabled: false,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,7 +244,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
isCropping: { browser: false, export: false, server: false },
|
isCropping: { browser: false, export: false, server: false },
|
||||||
croppingElementId: { browser: false, export: false, server: false },
|
croppingElementId: { browser: false, export: false, server: false },
|
||||||
searchMatches: { browser: false, export: false, server: false },
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
lassoSelectionEnabled: { browser: true, export: false, server: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
import { CLASSES } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import { alignActionsPredicate } from "../actions/actionAlign";
|
|
||||||
|
|
||||||
export const canChangeStrokeColor = (
|
export const canChangeStrokeColor = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@@ -91,12 +90,10 @@ export const SelectedShapeActions = ({
|
|||||||
appState,
|
appState,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
renderAction,
|
renderAction,
|
||||||
app,
|
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
app: AppClassProperties;
|
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(elementsMap, appState);
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
|
||||||
@@ -136,9 +133,6 @@ export const SelectedShapeActions = ({
|
|||||||
targetElements.length === 1 &&
|
targetElements.length === 1 &&
|
||||||
isImageElement(targetElements[0]);
|
isImageElement(targetElements[0]);
|
||||||
|
|
||||||
const showAlignActions =
|
|
||||||
!isSingleElementBoundContainer && alignActionsPredicate(appState, app);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<div>
|
<div>
|
||||||
@@ -206,7 +200,7 @@ export const SelectedShapeActions = ({
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{showAlignActions && !isSingleElementBoundContainer && (
|
{targetElements.length > 1 && !isSingleElementBoundContainer && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.align")}</legend>
|
<legend>{t("labels.align")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
|
|||||||
@@ -465,7 +465,6 @@ import { cropElement } from "../element/cropElement";
|
|||||||
import { wrapText } from "../element/textWrapping";
|
import { wrapText } from "../element/textWrapping";
|
||||||
import { actionCopyElementLink } from "../actions/actionElementLink";
|
import { actionCopyElementLink } from "../actions/actionElementLink";
|
||||||
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
||||||
import { LassoTrail } from "../lasso";
|
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -636,8 +635,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: "rgba(255, 255, 255, 0.2)",
|
: "rgba(255, 255, 255, 0.2)",
|
||||||
});
|
});
|
||||||
|
|
||||||
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
|
|
||||||
|
|
||||||
onChangeEmitter = new Emitter<
|
onChangeEmitter = new Emitter<
|
||||||
[
|
[
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -1610,11 +1607,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
<SVGLayer
|
<SVGLayer
|
||||||
trails={[
|
trails={[this.laserTrails, this.eraserTrail]}
|
||||||
this.laserTrails,
|
|
||||||
this.eraserTrail,
|
|
||||||
this.lassoTrail,
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
{selectedElements.length === 1 &&
|
{selectedElements.length === 1 &&
|
||||||
this.state.openDialog?.name !==
|
this.state.openDialog?.name !==
|
||||||
@@ -3242,12 +3235,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
newElements,
|
newElements,
|
||||||
topLayerFrame,
|
topLayerFrame,
|
||||||
);
|
);
|
||||||
addElementsToFrame(
|
addElementsToFrame(nextElements, eligibleElements, topLayerFrame);
|
||||||
nextElements,
|
|
||||||
eligibleElements,
|
|
||||||
topLayerFrame,
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
@@ -4338,14 +4326,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
mutateElement(
|
mutateElement(element, {
|
||||||
element,
|
x: element.x + offsetX,
|
||||||
{
|
y: element.y + offsetY,
|
||||||
x: element.x + offsetX,
|
});
|
||||||
y: element.y + offsetY,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: selectedElements,
|
||||||
@@ -4362,8 +4346,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.triggerUpdate();
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ENTER) {
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
@@ -4522,14 +4504,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === KEYS[1] && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
|
||||||
if (this.state.activeTool.type === "selection") {
|
|
||||||
this.setActiveTool({ type: "lassoSelection" });
|
|
||||||
} else {
|
|
||||||
this.setActiveTool({ type: "selection" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||||
@@ -6560,15 +6534,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.activeTool.type,
|
this.state.activeTool.type,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
} else if (this.state.activeTool.type === "lassoSelection") {
|
|
||||||
// Begin a mark capture. This does not have to update state yet.
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.lassoTrail.startPath(gridX, gridY);
|
|
||||||
} else if (this.state.activeTool.type === "custom") {
|
} else if (this.state.activeTool.type === "custom") {
|
||||||
setCursorForShape(this.interactiveCanvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -8355,7 +8320,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState({ snapLines });
|
flushSync(() => {
|
||||||
|
this.setState({ snapLines });
|
||||||
|
});
|
||||||
|
|
||||||
// when we're editing the name of a frame, we want the user to be
|
// when we're editing the name of a frame, we want the user to be
|
||||||
// able to select and interact with the text input
|
// able to select and interact with the text input
|
||||||
@@ -8485,63 +8452,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
} else if (this.state.activeTool.type === "lassoSelection") {
|
|
||||||
const { intersectedElementIds, enclosedElementIds } =
|
|
||||||
this.lassoTrail.addPointToPath(pointerCoords.x, pointerCoords.y);
|
|
||||||
|
|
||||||
this.setState((prevState) => {
|
|
||||||
const elements = [...intersectedElementIds, ...enclosedElementIds];
|
|
||||||
|
|
||||||
const nextSelectedElementIds = elements.reduce((acc, id) => {
|
|
||||||
acc[id] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<ExcalidrawElement["id"], true>);
|
|
||||||
|
|
||||||
const nextSelectedGroupIds = selectGroupsForSelectedElements(
|
|
||||||
{
|
|
||||||
selectedElementIds: nextSelectedElementIds,
|
|
||||||
editingGroupId: prevState.editingGroupId,
|
|
||||||
},
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
prevState,
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: not entirely correct (need to select all elements in group instead)
|
|
||||||
for (const [id, selected] of Object.entries(nextSelectedElementIds)) {
|
|
||||||
if (selected) {
|
|
||||||
const element = this.scene.getNonDeletedElement(id);
|
|
||||||
if (element && element.groupIds.length > 0) {
|
|
||||||
delete nextSelectedElementIds[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make elegant and decide if all children are selected, do we keep?
|
|
||||||
for (const [id, selected] of Object.entries(nextSelectedElementIds)) {
|
|
||||||
if (selected) {
|
|
||||||
const element = this.scene.getNonDeletedElement(id);
|
|
||||||
|
|
||||||
if (element && isFrameLikeElement(element)) {
|
|
||||||
const elementsInFrame = getFrameChildren(
|
|
||||||
elementsMap,
|
|
||||||
element.id,
|
|
||||||
);
|
|
||||||
for (const child of elementsInFrame) {
|
|
||||||
delete nextSelectedElementIds[child.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
nextSelectedElementIds,
|
|
||||||
prevState,
|
|
||||||
),
|
|
||||||
selectedGroupIds: nextSelectedGroupIds.selectedGroupIds,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// It is very important to read this.state within each move event,
|
// It is very important to read this.state within each move event,
|
||||||
// otherwise we would read a stale one!
|
// otherwise we would read a stale one!
|
||||||
@@ -8678,7 +8588,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements,
|
elements,
|
||||||
this.state.selectionElement,
|
this.state.selectionElement,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -8796,8 +8705,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
originSnapOffset: null,
|
originSnapOffset: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.lassoTrail.endPath();
|
|
||||||
|
|
||||||
this.lastPointerMoveCoords = null;
|
this.lastPointerMoveCoords = null;
|
||||||
|
|
||||||
SnapCache.setReferenceSnapPoints(null);
|
SnapCache.setReferenceSnapPoints(null);
|
||||||
@@ -8964,7 +8871,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isImageElement(newElement)) {
|
if (isImageElement(newElement)) {
|
||||||
const imageElement = newElement;
|
const imageElement = newElement;
|
||||||
try {
|
try {
|
||||||
@@ -9110,7 +9016,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getElementsMapIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
elementsInsideFrame,
|
elementsInsideFrame,
|
||||||
newElement,
|
newElement,
|
||||||
this.state,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9228,7 +9133,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
nextElements,
|
nextElements,
|
||||||
elementsToAdd,
|
elementsToAdd,
|
||||||
topLayerFrame,
|
topLayerFrame,
|
||||||
this.state,
|
|
||||||
);
|
);
|
||||||
} else if (!topLayerFrame) {
|
} else if (!topLayerFrame) {
|
||||||
if (this.state.editingGroupId) {
|
if (this.state.editingGroupId) {
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ const LayerUI = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
app={app}
|
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export const MobileMenu = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
app={app}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import "./Stats.scss";
|
|||||||
import { isGridModeEnabled } from "../../snapping";
|
import { isGridModeEnabled } from "../../snapping";
|
||||||
import { getUncroppedWidthAndHeight } from "../../element/cropElement";
|
import { getUncroppedWidthAndHeight } from "../../element/cropElement";
|
||||||
import { round } from "../../../math";
|
import { round } from "../../../math";
|
||||||
import { frameAndChildrenSelectedTogether } from "../../frame";
|
|
||||||
|
|
||||||
interface StatsProps {
|
interface StatsProps {
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
@@ -171,10 +170,6 @@ export const StatsInner = memo(
|
|||||||
return getAtomicUnits(selectedElements, appState);
|
return getAtomicUnits(selectedElements, appState);
|
||||||
}, [selectedElements, appState]);
|
}, [selectedElements, appState]);
|
||||||
|
|
||||||
const _frameAndChildrenSelectedTogether = useMemo(() => {
|
|
||||||
return frameAndChildrenSelectedTogether(selectedElements);
|
|
||||||
}, [selectedElements]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="exc-stats">
|
<div className="exc-stats">
|
||||||
<Island padding={3}>
|
<Island padding={3}>
|
||||||
@@ -231,7 +226,7 @@ export const StatsInner = memo(
|
|||||||
{renderCustomStats?.(elements, appState)}
|
{renderCustomStats?.(elements, appState)}
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{!_frameAndChildrenSelectedTogether && selectedElements.length > 0 && (
|
{selectedElements.length > 0 && (
|
||||||
<div
|
<div
|
||||||
id="elementStats"
|
id="elementStats"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -55,152 +55,146 @@ type ToolButtonProps =
|
|||||||
onPointerDown?(data: { pointerType: PointerType }): void;
|
onPointerDown?(data: { pointerType: PointerType }): void;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ToolButton = React.forwardRef(
|
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||||
(
|
const { id: excalId } = useExcalidrawContainer();
|
||||||
{
|
const innerRef = React.useRef(null);
|
||||||
size = "medium",
|
React.useImperativeHandle(ref, () => innerRef.current);
|
||||||
visible = true,
|
const sizeCn = `ToolIcon_size_${props.size}`;
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
}: ToolButtonProps,
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const { id: excalId } = useExcalidrawContainer();
|
|
||||||
const innerRef = React.useRef(null);
|
|
||||||
React.useImperativeHandle(ref, () => innerRef.current);
|
|
||||||
const sizeCn = `ToolIcon_size_${size}`;
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
const onClick = async (event: React.MouseEvent) => {
|
const onClick = async (event: React.MouseEvent) => {
|
||||||
const ret = "onClick" in props && props.onClick?.(event);
|
const ret = "onClick" in props && props.onClick?.(event);
|
||||||
|
|
||||||
if (isPromiseLike(ret)) {
|
if (isPromiseLike(ret)) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await ret;
|
await ret;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (!(error instanceof AbortError)) {
|
if (!(error instanceof AbortError)) {
|
||||||
throw error;
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isMountedRef.current = true;
|
|
||||||
return () => {
|
|
||||||
isMountedRef.current = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.type === "button" ||
|
|
||||||
props.type === "icon" ||
|
|
||||||
props.type === "submit"
|
|
||||||
) {
|
|
||||||
const type = (props.type === "icon" ? "button" : props.type) as
|
|
||||||
| "button"
|
|
||||||
| "submit";
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"ToolIcon_type_button",
|
|
||||||
sizeCn,
|
|
||||||
className,
|
|
||||||
visible && !props.hidden
|
|
||||||
? "ToolIcon_type_button--show"
|
|
||||||
: "ToolIcon_type_button--hide",
|
|
||||||
{
|
|
||||||
ToolIcon: !props.hidden,
|
|
||||||
"ToolIcon--selected": props.selected,
|
|
||||||
"ToolIcon--plain": props.type === "icon",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
style={props.style}
|
|
||||||
data-testid={props["data-testid"]}
|
|
||||||
hidden={props.hidden}
|
|
||||||
title={props.title}
|
|
||||||
aria-label={props["aria-label"]}
|
|
||||||
type={type}
|
|
||||||
onClick={onClick}
|
|
||||||
ref={innerRef}
|
|
||||||
disabled={isLoading || props.isLoading || !!props.disabled}
|
|
||||||
>
|
|
||||||
{(props.icon || props.label) && (
|
|
||||||
<div
|
|
||||||
className="ToolIcon__icon"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-disabled={!!props.disabled}
|
|
||||||
>
|
|
||||||
{props.icon || props.label}
|
|
||||||
{props.keyBindingLabel && (
|
|
||||||
<span className="ToolIcon__keybinding">
|
|
||||||
{props.keyBindingLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{props.isLoading && <Spinner />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.showAriaLabel && (
|
|
||||||
<div className="ToolIcon__label">
|
|
||||||
{props["aria-label"]} {isLoading && <Spinner />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.type === "button" ||
|
||||||
|
props.type === "icon" ||
|
||||||
|
props.type === "submit"
|
||||||
|
) {
|
||||||
|
const type = (props.type === "icon" ? "button" : props.type) as
|
||||||
|
| "button"
|
||||||
|
| "submit";
|
||||||
return (
|
return (
|
||||||
<label
|
<button
|
||||||
className={clsx("ToolIcon", className)}
|
className={clsx(
|
||||||
|
"ToolIcon_type_button",
|
||||||
|
sizeCn,
|
||||||
|
props.className,
|
||||||
|
props.visible && !props.hidden
|
||||||
|
? "ToolIcon_type_button--show"
|
||||||
|
: "ToolIcon_type_button--hide",
|
||||||
|
{
|
||||||
|
ToolIcon: !props.hidden,
|
||||||
|
"ToolIcon--selected": props.selected,
|
||||||
|
"ToolIcon--plain": props.type === "icon",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={props.style}
|
||||||
|
data-testid={props["data-testid"]}
|
||||||
|
hidden={props.hidden}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
onPointerDown={(event) => {
|
aria-label={props["aria-label"]}
|
||||||
lastPointerTypeRef.current = event.pointerType || null;
|
type={type}
|
||||||
props.onPointerDown?.({ pointerType: event.pointerType || null });
|
onClick={onClick}
|
||||||
}}
|
ref={innerRef}
|
||||||
onPointerUp={() => {
|
disabled={isLoading || props.isLoading || !!props.disabled}
|
||||||
requestAnimationFrame(() => {
|
|
||||||
lastPointerTypeRef.current = null;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
{(props.icon || props.label) && (
|
||||||
className={`ToolIcon_type_radio ${sizeCn}`}
|
<div
|
||||||
type="radio"
|
className="ToolIcon__icon"
|
||||||
name={props.name}
|
aria-hidden="true"
|
||||||
aria-label={props["aria-label"]}
|
aria-disabled={!!props.disabled}
|
||||||
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
>
|
||||||
data-testid={props["data-testid"]}
|
{props.icon || props.label}
|
||||||
id={`${excalId}-${props.id}`}
|
{props.keyBindingLabel && (
|
||||||
onChange={() => {
|
<span className="ToolIcon__keybinding">
|
||||||
props.onChange?.({ pointerType: lastPointerTypeRef.current });
|
{props.keyBindingLabel}
|
||||||
}}
|
</span>
|
||||||
checked={props.checked}
|
)}
|
||||||
ref={innerRef}
|
{props.isLoading && <Spinner />}
|
||||||
/>
|
</div>
|
||||||
<div className="ToolIcon__icon">
|
)}
|
||||||
{props.icon}
|
{props.showAriaLabel && (
|
||||||
{props.keyBindingLabel && (
|
<div className="ToolIcon__label">
|
||||||
<span className="ToolIcon__keybinding">
|
{props["aria-label"]} {isLoading && <Spinner />}
|
||||||
{props.keyBindingLabel}
|
</div>
|
||||||
</span>
|
)}
|
||||||
)}
|
{props.children}
|
||||||
</div>
|
</button>
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={clsx("ToolIcon", props.className)}
|
||||||
|
title={props.title}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
lastPointerTypeRef.current = event.pointerType || null;
|
||||||
|
props.onPointerDown?.({ pointerType: event.pointerType || null });
|
||||||
|
}}
|
||||||
|
onPointerUp={() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
lastPointerTypeRef.current = null;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={`ToolIcon_type_radio ${sizeCn}`}
|
||||||
|
type="radio"
|
||||||
|
name={props.name}
|
||||||
|
aria-label={props["aria-label"]}
|
||||||
|
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
||||||
|
data-testid={props["data-testid"]}
|
||||||
|
id={`${excalId}-${props.id}`}
|
||||||
|
onChange={() => {
|
||||||
|
props.onChange?.({ pointerType: lastPointerTypeRef.current });
|
||||||
|
}}
|
||||||
|
checked={props.checked}
|
||||||
|
ref={innerRef}
|
||||||
|
/>
|
||||||
|
<div className="ToolIcon__icon">
|
||||||
|
{props.icon}
|
||||||
|
{props.keyBindingLabel && (
|
||||||
|
<span className="ToolIcon__keybinding">{props.keyBindingLabel}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToolButton.defaultProps = {
|
||||||
|
visible: true,
|
||||||
|
className: "",
|
||||||
|
size: "medium",
|
||||||
|
};
|
||||||
|
|
||||||
ToolButton.displayName = "ToolButton";
|
ToolButton.displayName = "ToolButton";
|
||||||
|
|||||||
@@ -171,17 +171,15 @@ export const Hyperlink = ({
|
|||||||
}, [handleSubmit]);
|
}, [handleSubmit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let timeoutId: number | null = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isEditing &&
|
inputRef &&
|
||||||
inputRef?.current &&
|
inputRef.current &&
|
||||||
!(device.viewport.isMobile || device.isTouchScreen)
|
!(device.viewport.isMobile || device.isTouchScreen)
|
||||||
) {
|
) {
|
||||||
inputRef.current.select();
|
inputRef.current.select();
|
||||||
}
|
}
|
||||||
}, [isEditing, device.viewport.isMobile, device.isTouchScreen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeoutId: number | null = null;
|
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -209,7 +207,15 @@ export const Hyperlink = ({
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [appState, element, isEditing, setAppState, elementsMap]);
|
}, [
|
||||||
|
appState,
|
||||||
|
element,
|
||||||
|
isEditing,
|
||||||
|
setAppState,
|
||||||
|
elementsMap,
|
||||||
|
device.viewport.isMobile,
|
||||||
|
device.isTouchScreen,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
trackEvent("hyperlink", "delete");
|
trackEvent("hyperlink", "delete");
|
||||||
|
|||||||
@@ -417,7 +417,6 @@ export const LIBRARY_DISABLED_TYPES = new Set([
|
|||||||
// use these constants to easily identify reference sites
|
// use these constants to easily identify reference sites
|
||||||
export const TOOL_TYPE = {
|
export const TOOL_TYPE = {
|
||||||
selection: "selection",
|
selection: "selection",
|
||||||
lassoSelection: "lassoSelection",
|
|
||||||
rectangle: "rectangle",
|
rectangle: "rectangle",
|
||||||
diamond: "diamond",
|
diamond: "diamond",
|
||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
|
|
||||||
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||||
mimeTypes.push(MIME_TYPES[type]);
|
mimeTypes.push(MIME_TYPES[type]);
|
||||||
|
|
||||||
return mimeTypes;
|
return mimeTypes;
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
@@ -39,34 +38,62 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
mimeTypes,
|
mimeTypes,
|
||||||
multiple: opts.multiple ?? false,
|
multiple: opts.multiple ?? false,
|
||||||
legacySetup: (resolve, reject, input) => {
|
legacySetup: (resolve, reject, input) => {
|
||||||
|
let isResolved = false;
|
||||||
|
let checkInterval: number | null = null;
|
||||||
|
|
||||||
|
// Increased delay for iOS to ensure file selection is complete
|
||||||
|
const CHECK_INTERVAL = 100; // 100ms
|
||||||
|
const MAX_CHECKS = 50; // 5 seconds total
|
||||||
|
let checkCount = 0;
|
||||||
|
|
||||||
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
||||||
const focusHandler = () => {
|
|
||||||
checkForFile();
|
|
||||||
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
|
||||||
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
|
||||||
scheduleRejection();
|
|
||||||
};
|
|
||||||
const checkForFile = () => {
|
const checkForFile = () => {
|
||||||
// this hack might not work when expecting multiple files
|
if (isResolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
|
isResolved = true;
|
||||||
const ret = opts.multiple ? [...input.files] : input.files[0];
|
const ret = opts.multiple ? [...input.files] : input.files[0];
|
||||||
resolve(ret as RetType);
|
resolve(ret as RetType);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkCount++;
|
||||||
|
if (checkCount >= MAX_CHECKS) {
|
||||||
|
scheduleRejection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const focusHandler = () => {
|
||||||
|
// Start checking for files only after focus event
|
||||||
|
checkInterval = window.setInterval(() => {
|
||||||
|
if (checkForFile()) {
|
||||||
|
clearInterval(checkInterval!);
|
||||||
|
}
|
||||||
|
}, CHECK_INTERVAL);
|
||||||
|
|
||||||
|
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
||||||
|
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
|
};
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
window.addEventListener(EVENT.FOCUS, focusHandler);
|
window.addEventListener(EVENT.FOCUS, focusHandler);
|
||||||
});
|
});
|
||||||
const interval = window.setInterval(() => {
|
|
||||||
checkForFile();
|
|
||||||
}, INPUT_CHANGE_INTERVAL_MS);
|
|
||||||
return (rejectPromise) => {
|
return (rejectPromise) => {
|
||||||
clearInterval(interval);
|
if (checkInterval) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}
|
||||||
scheduleRejection.cancel();
|
scheduleRejection.cancel();
|
||||||
window.removeEventListener(EVENT.FOCUS, focusHandler);
|
window.removeEventListener(EVENT.FOCUS, focusHandler);
|
||||||
document.removeEventListener(EVENT.KEYUP, scheduleRejection);
|
document.removeEventListener(EVENT.KEYUP, scheduleRejection);
|
||||||
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
if (rejectPromise) {
|
|
||||||
// so that something is shown in console if we need to debug this
|
if (rejectPromise && !isResolved) {
|
||||||
console.warn("Opening the file was canceled (legacy-fs).");
|
console.warn("Opening the file was canceled (legacy-fs).");
|
||||||
rejectPromise(new AbortError());
|
rejectPromise(new AbortError());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { validateLibraryUrl } from "./library";
|
|
||||||
|
|
||||||
describe("validateLibraryUrl", () => {
|
|
||||||
it("should validate hostname & pathname", () => {
|
|
||||||
// valid hostnames
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://www.excalidraw.com", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://library.excalidraw.com", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://library.excalidraw.com", [
|
|
||||||
"library.excalidraw.com",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com/"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com", ["excalidraw.com/"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
// valid pathnames
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/path", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/path/", ["excalidraw.com"]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/path", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/path/", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/path", [
|
|
||||||
"excalidraw.com/specific/path/",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/path/other", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
// invalid hostnames
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://xexcalidraw.com", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://x-excalidraw.com", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.comx", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.com.mx", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
// protocol must be https
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("http://excalidraw.com.mx", ["excalidraw.com"]),
|
|
||||||
).toThrow();
|
|
||||||
|
|
||||||
// invalid pathnames
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/other/path", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/paths", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.com/specific/path-s", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
validateLibraryUrl("https://excalidraw.com/some/specific/path", [
|
|
||||||
"excalidraw.com/specific/path",
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -36,18 +36,7 @@ import { Queue } from "../queue";
|
|||||||
import { hashElementsVersion, hashString } from "../element";
|
import { hashElementsVersion, hashString } from "../element";
|
||||||
import { toValidURL } from "./url";
|
import { toValidURL } from "./url";
|
||||||
|
|
||||||
/**
|
const ALLOWED_LIBRARY_HOSTNAMES = ["excalidraw.com"];
|
||||||
* format: hostname or hostname/pathname
|
|
||||||
*
|
|
||||||
* Both hostname and pathname are matched partially,
|
|
||||||
* hostname from the end, pathname from the start, with subdomain/path
|
|
||||||
* boundaries
|
|
||||||
**/
|
|
||||||
const ALLOWED_LIBRARY_URLS = [
|
|
||||||
"excalidraw.com",
|
|
||||||
// when installing from github PRs
|
|
||||||
"raw.githubusercontent.com/excalidraw/excalidraw-libraries",
|
|
||||||
];
|
|
||||||
|
|
||||||
type LibraryUpdate = {
|
type LibraryUpdate = {
|
||||||
/** deleted library items since last onLibraryChange event */
|
/** deleted library items since last onLibraryChange event */
|
||||||
@@ -480,37 +469,26 @@ export const distributeLibraryItemsOnSquareGrid = (
|
|||||||
return resElements;
|
return resElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateLibraryUrl = (
|
const validateLibraryUrl = (
|
||||||
libraryUrl: string,
|
libraryUrl: string,
|
||||||
/**
|
/**
|
||||||
* @returns `true` if the URL is valid, throws otherwise.
|
* If supplied, takes precedence over the default whitelist.
|
||||||
|
* Return `true` if the URL is valid.
|
||||||
*/
|
*/
|
||||||
validator:
|
validator?: (libraryUrl: string) => boolean,
|
||||||
| ((libraryUrl: string) => boolean)
|
): boolean => {
|
||||||
| string[] = ALLOWED_LIBRARY_URLS,
|
|
||||||
): true => {
|
|
||||||
if (
|
if (
|
||||||
typeof validator === "function"
|
validator
|
||||||
? validator(libraryUrl)
|
? validator(libraryUrl)
|
||||||
: validator.some((allowedUrlDef) => {
|
: ALLOWED_LIBRARY_HOSTNAMES.includes(
|
||||||
const allowedUrl = new URL(
|
new URL(libraryUrl).hostname.split(".").slice(-2).join("."),
|
||||||
`https://${allowedUrlDef.replace(/^https?:\/\//, "")}`,
|
)
|
||||||
);
|
|
||||||
|
|
||||||
const { hostname, pathname } = new URL(libraryUrl);
|
|
||||||
|
|
||||||
return (
|
|
||||||
new RegExp(`(^|\\.)${allowedUrl.hostname}$`).test(hostname) &&
|
|
||||||
new RegExp(
|
|
||||||
`^${allowedUrl.pathname.replace(/\/+$/, "")}(/+|$)`,
|
|
||||||
).test(pathname)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Invalid or disallowed library URL: "${libraryUrl}"`);
|
console.error(`Invalid or disallowed library URL: "${libraryUrl}"`);
|
||||||
|
throw new Error("Invalid or disallowed library URL");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseLibraryTokensFromUrl = () => {
|
export const parseLibraryTokensFromUrl = () => {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||||||
boolean
|
boolean
|
||||||
> = {
|
> = {
|
||||||
selection: true,
|
selection: true,
|
||||||
lassoSelection: true,
|
|
||||||
text: true,
|
text: true,
|
||||||
rectangle: true,
|
rectangle: true,
|
||||||
diamond: true,
|
diamond: true,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { generateRoughOptions } from "../scene/Shape";
|
|||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isFrameLikeElement,
|
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@@ -325,15 +324,6 @@ export const getElementLineSegments = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFrameLikeElement(element)) {
|
|
||||||
return [
|
|
||||||
lineSegment(nw, ne),
|
|
||||||
lineSegment(ne, se),
|
|
||||||
lineSegment(se, sw),
|
|
||||||
lineSegment(sw, nw),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
lineSegment(nw, ne),
|
lineSegment(nw, ne),
|
||||||
lineSegment(sw, se),
|
lineSegment(sw, se),
|
||||||
|
|||||||
@@ -909,20 +909,6 @@ export const updateElbowArrowPoints = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0. During all element replacement in the scene, we just need to renormalize
|
|
||||||
// the arrow
|
|
||||||
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
|
||||||
if (elementsMap.size === 0 && updates.points) {
|
|
||||||
return normalizeArrowElementUpdate(
|
|
||||||
updates.points.map((p) =>
|
|
||||||
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
|
|
||||||
),
|
|
||||||
arrow.fixedSegments,
|
|
||||||
arrow.startIsSpecial,
|
|
||||||
arrow.endIsSpecial,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPoints: readonly LocalPoint[] = updates.points
|
const updatedPoints: readonly LocalPoint[] = updates.points
|
||||||
? updates.points && updates.points.length === 2
|
? updates.points && updates.points.length === 2
|
||||||
? arrow.points.map((p, idx) =>
|
? arrow.points.map((p, idx) =>
|
||||||
|
|||||||
@@ -254,9 +254,6 @@ const addNewNode = (
|
|||||||
backgroundColor: element.backgroundColor,
|
backgroundColor: element.backgroundColor,
|
||||||
strokeColor: element.strokeColor,
|
strokeColor: element.strokeColor,
|
||||||
strokeWidth: element.strokeWidth,
|
strokeWidth: element.strokeWidth,
|
||||||
opacity: element.opacity,
|
|
||||||
fillStyle: element.fillStyle,
|
|
||||||
strokeStyle: element.strokeStyle,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
@@ -332,9 +329,6 @@ export const addNewNodes = (
|
|||||||
backgroundColor: startNode.backgroundColor,
|
backgroundColor: startNode.backgroundColor,
|
||||||
strokeColor: startNode.strokeColor,
|
strokeColor: startNode.strokeColor,
|
||||||
strokeWidth: startNode.strokeWidth,
|
strokeWidth: startNode.strokeWidth,
|
||||||
opacity: startNode.opacity,
|
|
||||||
fillStyle: startNode.fillStyle,
|
|
||||||
strokeStyle: startNode.strokeStyle,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
@@ -422,13 +416,11 @@ const createBindingArrow = (
|
|||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: startX,
|
x: startX,
|
||||||
y: startY,
|
y: startY,
|
||||||
startArrowhead: null,
|
startArrowhead: appState.currentItemStartArrowhead,
|
||||||
endArrowhead: appState.currentItemEndArrowhead,
|
endArrowhead: appState.currentItemEndArrowhead,
|
||||||
strokeColor: startBindingElement.strokeColor,
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
strokeStyle: startBindingElement.strokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
strokeWidth: startBindingElement.strokeWidth,
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
opacity: startBindingElement.opacity,
|
|
||||||
roughness: startBindingElement.roughness,
|
|
||||||
points: [pointFrom(0, 0), pointFrom(endX, endY)],
|
points: [pointFrom(0, 0), pointFrom(endX, endY)],
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|||||||
+43
-213
@@ -95,11 +95,12 @@ export const getElementsCompletelyInFrame = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const isElementContainingFrame = (
|
export const isElementContainingFrame = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return getElementsWithinSelection([frame], element, elementsMap).some(
|
return getElementsWithinSelection(elements, element, elementsMap).some(
|
||||||
(e) => e.id === frame.id,
|
(e) => e.id === frame.id,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -143,7 +144,7 @@ export const elementOverlapsWithFrame = (
|
|||||||
return (
|
return (
|
||||||
elementsAreInFrameBounds([element], frame, elementsMap) ||
|
elementsAreInFrameBounds([element], frame, elementsMap) ||
|
||||||
isElementIntersectingFrame(element, frame, elementsMap) ||
|
isElementIntersectingFrame(element, frame, elementsMap) ||
|
||||||
isElementContainingFrame(element, frame, elementsMap)
|
isElementContainingFrame([frame], element, frame, elementsMap)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ export const getElementsInResizingFrame = (
|
|||||||
const elementsCompletelyInFrame = new Set([
|
const elementsCompletelyInFrame = new Set([
|
||||||
...getElementsCompletelyInFrame(allElements, frame, elementsMap),
|
...getElementsCompletelyInFrame(allElements, frame, elementsMap),
|
||||||
...prevElementsInFrame.filter((element) =>
|
...prevElementsInFrame.filter((element) =>
|
||||||
isElementContainingFrame(element, frame, elementsMap),
|
isElementContainingFrame(allElements, element, frame, elementsMap),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -369,57 +370,12 @@ export const getElementsInNewFrame = (
|
|||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return omitPartialGroups(
|
return omitGroupsContainingFrameLikes(
|
||||||
omitGroupsContainingFrameLikes(
|
elements,
|
||||||
elements,
|
getElementsCompletelyInFrame(elements, frame, elementsMap),
|
||||||
getElementsCompletelyInFrame(elements, frame, elementsMap),
|
|
||||||
),
|
|
||||||
frame,
|
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const omitPartialGroups = (
|
|
||||||
elements: ExcalidrawElement[],
|
|
||||||
frame: ExcalidrawFrameLikeElement,
|
|
||||||
allElementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
const elementsToReturn = [];
|
|
||||||
const checkedGroups = new Map<string, boolean>();
|
|
||||||
|
|
||||||
for (const element of elements) {
|
|
||||||
let shouldOmit = false;
|
|
||||||
if (element.groupIds.length > 0) {
|
|
||||||
// if some partial group should be omitted, then all elements in that group should be omitted
|
|
||||||
if (element.groupIds.some((gid) => checkedGroups.get(gid))) {
|
|
||||||
shouldOmit = true;
|
|
||||||
} else {
|
|
||||||
const allElementsInGroup = new Set(
|
|
||||||
element.groupIds.flatMap((gid) =>
|
|
||||||
getElementsInGroup(allElementsMap, gid),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
shouldOmit = !elementsAreInFrameBounds(
|
|
||||||
Array.from(allElementsInGroup),
|
|
||||||
frame,
|
|
||||||
allElementsMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.groupIds.forEach((gid) => {
|
|
||||||
checkedGroups.set(gid, shouldOmit);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldOmit) {
|
|
||||||
elementsToReturn.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return elementsToReturn;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContainingFrame = (
|
export const getContainingFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
@@ -498,7 +454,6 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
|||||||
allElements: T,
|
allElements: T,
|
||||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
appState: AppState,
|
|
||||||
): T => {
|
): T => {
|
||||||
const elementsMap = arrayToMap(allElements);
|
const elementsMap = arrayToMap(allElements);
|
||||||
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
|
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
|
||||||
@@ -534,17 +489,6 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the element is already in another frame (which is also in elementsToAdd),
|
|
||||||
// it means that frame and children are selected at the same time
|
|
||||||
// => keep original frame membership, do not add to the target frame
|
|
||||||
if (
|
|
||||||
element.frameId &&
|
|
||||||
appState.selectedElementIds[element.id] &&
|
|
||||||
appState.selectedElementIds[element.frameId]
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||||
finalElementsToAdd.push(element);
|
finalElementsToAdd.push(element);
|
||||||
}
|
}
|
||||||
@@ -633,7 +577,6 @@ export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
|
|||||||
removeAllElementsFromFrame(allElements, frame),
|
removeAllElementsFromFrame(allElements, frame),
|
||||||
nextElementsInFrame,
|
nextElementsInFrame,
|
||||||
frame,
|
frame,
|
||||||
app.state,
|
|
||||||
).slice();
|
).slice();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -740,16 +683,6 @@ export const getTargetFrame = (
|
|||||||
? getContainerElement(element, elementsMap) || element
|
? getContainerElement(element, elementsMap) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
// if the element and its containing frame are both selected, then
|
|
||||||
// the containing frame is the target frame
|
|
||||||
if (
|
|
||||||
_element.frameId &&
|
|
||||||
appState.selectedElementIds[_element.id] &&
|
|
||||||
appState.selectedElementIds[_element.frameId]
|
|
||||||
) {
|
|
||||||
return getContainingFrame(_element, elementsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
return appState.selectedElementIds[_element.id] &&
|
return appState.selectedElementIds[_element.id] &&
|
||||||
appState.selectedElementsAreBeingDragged
|
appState.selectedElementsAreBeingDragged
|
||||||
? appState.frameToHighlight
|
? appState.frameToHighlight
|
||||||
@@ -762,151 +695,61 @@ export const isElementInFrame = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
allElementsMap: ElementsMap,
|
allElementsMap: ElementsMap,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
opts?: {
|
|
||||||
targetFrame?: ExcalidrawFrameLikeElement;
|
|
||||||
checkedGroups?: Map<string, boolean>;
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
const frame =
|
const frame = getTargetFrame(element, allElementsMap, appState);
|
||||||
opts?.targetFrame ?? getTargetFrame(element, allElementsMap, appState);
|
|
||||||
|
|
||||||
if (!frame) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element, allElementsMap) || element
|
? getContainerElement(element, allElementsMap) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
const setGroupsInFrame = (isInFrame: boolean) => {
|
if (frame) {
|
||||||
if (opts?.checkedGroups) {
|
// Perf improvement:
|
||||||
_element.groupIds.forEach((groupId) => {
|
// For an element that's already in a frame, if it's not being dragged
|
||||||
opts.checkedGroups?.set(groupId, isInFrame);
|
// 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.selectedElementsAreBeingDragged
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
if (_element.groupIds.length === 0) {
|
||||||
// if the element is not selected, or it is selected but not being dragged,
|
return elementOverlapsWithFrame(_element, frame, allElementsMap);
|
||||||
// frame membership won't update, so return true
|
|
||||||
!appState.selectedElementIds[_element.id] ||
|
|
||||||
!appState.selectedElementsAreBeingDragged ||
|
|
||||||
// if both frame and element are selected, won't update membership, so return true
|
|
||||||
(appState.selectedElementIds[_element.id] &&
|
|
||||||
appState.selectedElementIds[frame.id])
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_element.groupIds.length === 0) {
|
|
||||||
return elementOverlapsWithFrame(_element, frame, allElementsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const gid of _element.groupIds) {
|
|
||||||
if (opts?.checkedGroups?.has(gid)) {
|
|
||||||
return opts.checkedGroups.get(gid)!!;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const allElementsInGroup = new Set(
|
const allElementsInGroup = new Set(
|
||||||
_element.groupIds
|
_element.groupIds.flatMap((gid) =>
|
||||||
.filter((gid) => {
|
getElementsInGroup(allElementsMap, gid),
|
||||||
if (opts?.checkedGroups) {
|
),
|
||||||
return !opts.checkedGroups.has(gid);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.flatMap((gid) => getElementsInGroup(allElementsMap, gid)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
|
||||||
const selectedElements = new Set(
|
|
||||||
getSelectedElements(allElementsMap, appState),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
|
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
||||||
|
const selectedElements = new Set(
|
||||||
|
getSelectedElements(allElementsMap, appState),
|
||||||
|
);
|
||||||
|
|
||||||
if (editingGroupOverlapsFrame) {
|
const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedElements.forEach((selectedElement) => {
|
if (editingGroupOverlapsFrame) {
|
||||||
allElementsInGroup.delete(selectedElement);
|
return true;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
|
||||||
if (isFrameLikeElement(elementInGroup)) {
|
|
||||||
setGroupsInFrame(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
|
||||||
if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
|
|
||||||
setGroupsInFrame(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shouldApplyFrameClip = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
frame: ExcalidrawFrameLikeElement,
|
|
||||||
appState: StaticCanvasAppState,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
checkedGroups?: Map<string, boolean>,
|
|
||||||
) => {
|
|
||||||
if (!appState.frameRendering || !appState.frameRendering.clip) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for individual elements, only clip when the element is
|
|
||||||
// a. overlapping with the frame, or
|
|
||||||
// b. containing the frame, for example when an element is used as a background
|
|
||||||
// and is therefore bigger than the frame and completely contains the frame
|
|
||||||
const shouldClipElementItself =
|
|
||||||
isElementIntersectingFrame(element, frame, elementsMap) ||
|
|
||||||
isElementContainingFrame(element, frame, elementsMap);
|
|
||||||
|
|
||||||
if (shouldClipElementItself) {
|
|
||||||
for (const groupId of element.groupIds) {
|
|
||||||
checkedGroups?.set(groupId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if an element is outside the frame, but is part of a group that has some elements
|
|
||||||
// "in" the frame, we should clip the element
|
|
||||||
if (
|
|
||||||
!shouldClipElementItself &&
|
|
||||||
element.groupIds.length > 0 &&
|
|
||||||
!elementsAreInFrameBounds([element], frame, elementsMap)
|
|
||||||
) {
|
|
||||||
let shouldClip = false;
|
|
||||||
|
|
||||||
// if no elements are being dragged, we can skip the geometry check
|
|
||||||
// because we know if the element is in the given frame or not
|
|
||||||
if (!appState.selectedElementsAreBeingDragged) {
|
|
||||||
shouldClip = element.frameId === frame.id;
|
|
||||||
for (const groupId of element.groupIds) {
|
|
||||||
checkedGroups?.set(groupId, shouldClip);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
shouldClip = isElementInFrame(element, elementsMap, appState, {
|
selectedElements.forEach((selectedElement) => {
|
||||||
targetFrame: frame,
|
allElementsInGroup.delete(selectedElement);
|
||||||
checkedGroups,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const groupId of element.groupIds) {
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
checkedGroups?.set(groupId, shouldClip);
|
if (isFrameLikeElement(elementInGroup)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldClip;
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
|
if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -936,16 +779,3 @@ export const getElementsOverlappingFrame = (
|
|||||||
.filter((el) => !el.frameId || el.frameId === frame.id)
|
.filter((el) => !el.frameId || el.frameId === frame.id)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const frameAndChildrenSelectedTogether = (
|
|
||||||
selectedElements: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
|
||||||
|
|
||||||
return (
|
|
||||||
selectedElements.length > 1 &&
|
|
||||||
selectedElements.some(
|
|
||||||
(element) => element.frameId && selectedElementsMap.has(element.frameId),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
/**
|
|
||||||
* all things related to lasso selection
|
|
||||||
* - lasso selection
|
|
||||||
* - intersection and enclosure checks
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type GlobalPoint,
|
|
||||||
type LineSegment,
|
|
||||||
type LocalPoint,
|
|
||||||
type Polygon,
|
|
||||||
pointFrom,
|
|
||||||
pointsEqual,
|
|
||||||
polygonFromPoints,
|
|
||||||
segmentsIntersectAt,
|
|
||||||
} from "../math";
|
|
||||||
import { isPointInShape } from "../utils/collision";
|
|
||||||
import {
|
|
||||||
type GeometricShape,
|
|
||||||
polylineFromPoints,
|
|
||||||
} from "../utils/geometry/shape";
|
|
||||||
import { AnimatedTrail } from "./animated-trail";
|
|
||||||
import { type AnimationFrameHandler } from "./animation-frame-handler";
|
|
||||||
import type App from "./components/App";
|
|
||||||
import { getElementLineSegments } from "./element/bounds";
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
|
||||||
import type { InteractiveCanvasRenderConfig } from "./scene/types";
|
|
||||||
import type { InteractiveCanvasAppState } from "./types";
|
|
||||||
import { easeOut } from "./utils";
|
|
||||||
|
|
||||||
export type LassoPath = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
points: LocalPoint[];
|
|
||||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
|
||||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderLassoSelection = (
|
|
||||||
lassoPath: LassoPath,
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
appState: InteractiveCanvasAppState,
|
|
||||||
selectionColor: InteractiveCanvasRenderConfig["selectionColor"],
|
|
||||||
) => {
|
|
||||||
context.save();
|
|
||||||
context.translate(
|
|
||||||
lassoPath.x + appState.scrollX,
|
|
||||||
lassoPath.y + appState.scrollY,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstPoint = lassoPath.points[0];
|
|
||||||
|
|
||||||
if (firstPoint) {
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(firstPoint[0], firstPoint[1]);
|
|
||||||
|
|
||||||
for (let i = 1; i < lassoPath.points.length; i++) {
|
|
||||||
context.lineTo(lassoPath.points[i][0], lassoPath.points[i][1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.strokeStyle = selectionColor;
|
|
||||||
context.lineWidth = 3 / appState.zoom.value;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lassoPath.points.length >= 3 &&
|
|
||||||
pointsEqual(
|
|
||||||
lassoPath.points[0],
|
|
||||||
lassoPath.points[lassoPath.points.length - 1],
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
context.closePath();
|
|
||||||
}
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
// export class LassoSelection {
|
|
||||||
// static createLassoPath = (x: number, y: number): LassoPath => {
|
|
||||||
// return {
|
|
||||||
// x,
|
|
||||||
// y,
|
|
||||||
// points: [],
|
|
||||||
// intersectedElements: new Set(),
|
|
||||||
// enclosedElements: new Set(),
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// static updateLassoPath = (
|
|
||||||
// lassoPath: LassoPath,
|
|
||||||
// pointerCoords: { x: number; y: number },
|
|
||||||
// elementsMap: ElementsMap,
|
|
||||||
// ): LassoPath => {
|
|
||||||
// const points = lassoPath.points;
|
|
||||||
// const dx = pointerCoords.x - lassoPath.x;
|
|
||||||
// const dy = pointerCoords.y - lassoPath.y;
|
|
||||||
|
|
||||||
// const lastPoint = points.length > 0 && points[points.length - 1];
|
|
||||||
// const discardPoint =
|
|
||||||
// lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
|
|
||||||
|
|
||||||
// if (!discardPoint) {
|
|
||||||
// const nextLassoPath = {
|
|
||||||
// ...lassoPath,
|
|
||||||
// points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // nextLassoPath.enclosedElements.clear();
|
|
||||||
|
|
||||||
// // const enclosedLassoPath = LassoSelection.closeLassoPath(
|
|
||||||
// // nextLassoPath,
|
|
||||||
// // elementsMap,
|
|
||||||
// // );
|
|
||||||
|
|
||||||
// // for (const [id, element] of elementsMap) {
|
|
||||||
// // if (!lassoPath.intersectedElements.has(element.id)) {
|
|
||||||
// // const intersects = intersect(nextLassoPath, element, elementsMap);
|
|
||||||
// // if (intersects) {
|
|
||||||
// // lassoPath.intersectedElements.add(element.id);
|
|
||||||
// // } else {
|
|
||||||
// // // check if the lasso path encloses the element
|
|
||||||
// // const enclosed = enclose(enclosedLassoPath, element, elementsMap);
|
|
||||||
// // if (enclosed) {
|
|
||||||
// // lassoPath.enclosedElements.add(element.id);
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// return nextLassoPath;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return lassoPath;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// private static closeLassoPath = (
|
|
||||||
// lassoPath: LassoPath,
|
|
||||||
// elementsMap: ElementsMap,
|
|
||||||
// ) => {
|
|
||||||
// const finalPoints = [...lassoPath.points, lassoPath.points[0]];
|
|
||||||
// // TODO: check if the lasso path encloses or intersects with any element
|
|
||||||
|
|
||||||
// const finalLassoPath = {
|
|
||||||
// ...lassoPath,
|
|
||||||
// points: finalPoints,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return finalLassoPath;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// static finalizeLassoPath = (
|
|
||||||
// lassoPath: LassoPath,
|
|
||||||
// elementsMap: ElementsMap,
|
|
||||||
// ) => {
|
|
||||||
// const enclosedLassoPath = LassoSelection.closeLassoPath(
|
|
||||||
// lassoPath,
|
|
||||||
// elementsMap,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// enclosedLassoPath.enclosedElements.clear();
|
|
||||||
// enclosedLassoPath.intersectedElements.clear();
|
|
||||||
|
|
||||||
// // for (const [id, element] of elementsMap) {
|
|
||||||
// // const intersects = intersect(enclosedLassoPath, element, elementsMap);
|
|
||||||
// // if (intersects) {
|
|
||||||
// // enclosedLassoPath.intersectedElements.add(element.id);
|
|
||||||
// // } else {
|
|
||||||
// // const enclosed = enclose(enclosedLassoPath, element, elementsMap);
|
|
||||||
// // if (enclosed) {
|
|
||||||
// // enclosedLassoPath.enclosedElements.add(element.id);
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// return enclosedLassoPath;
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
const intersectionTest = (
|
|
||||||
lassoPath: GlobalPoint[],
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): boolean => {
|
|
||||||
const elementLineSegments = getElementLineSegments(element, elementsMap);
|
|
||||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
const prevPoint = pointFrom<GlobalPoint>(
|
|
||||||
lassoPath[index - 1][0],
|
|
||||||
lassoPath[index - 1][1],
|
|
||||||
);
|
|
||||||
const currentPoint = pointFrom<GlobalPoint>(point[0], point[1]);
|
|
||||||
acc.push([prevPoint, currentPoint] as LineSegment<GlobalPoint>);
|
|
||||||
return acc;
|
|
||||||
}, [] as LineSegment<GlobalPoint>[]);
|
|
||||||
|
|
||||||
for (const lassoSegment of lassoSegments) {
|
|
||||||
for (const elementSegment of elementLineSegments) {
|
|
||||||
if (segmentsIntersectAt(lassoSegment, elementSegment)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enclosureTest = (
|
|
||||||
lassoPath: GlobalPoint[],
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): boolean => {
|
|
||||||
const polyline = polylineFromPoints(lassoPath);
|
|
||||||
|
|
||||||
const closedPathShape: GeometricShape<GlobalPoint> = {
|
|
||||||
type: "polygon",
|
|
||||||
data: polygonFromPoints(polyline.flat()),
|
|
||||||
} as {
|
|
||||||
type: "polygon";
|
|
||||||
data: Polygon<GlobalPoint>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const elementSegments = getElementLineSegments(element, elementsMap);
|
|
||||||
|
|
||||||
for (const segment of elementSegments) {
|
|
||||||
if (segment.some((point) => isPointInShape(point, closedPathShape))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LassoTrail extends AnimatedTrail {
|
|
||||||
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
|
||||||
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
|
|
||||||
|
|
||||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
|
||||||
super(animationFrameHandler, app, {
|
|
||||||
simplify: 0,
|
|
||||||
streamline: 0.4,
|
|
||||||
sizeMapping: (c) => {
|
|
||||||
const DECAY_TIME = Infinity;
|
|
||||||
const DECAY_LENGTH = 5000;
|
|
||||||
const t = Math.max(
|
|
||||||
0,
|
|
||||||
1 - (performance.now() - c.pressure) / DECAY_TIME,
|
|
||||||
);
|
|
||||||
const l =
|
|
||||||
(DECAY_LENGTH -
|
|
||||||
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
|
|
||||||
DECAY_LENGTH;
|
|
||||||
|
|
||||||
return Math.min(easeOut(l), easeOut(t));
|
|
||||||
},
|
|
||||||
fill: () => "rgb(0,118,255)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startPath(x: number, y: number) {
|
|
||||||
super.startPath(x, y);
|
|
||||||
this.intersectedElements.clear();
|
|
||||||
this.enclosedElements.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
addPointToPath(x: number, y: number) {
|
|
||||||
super.addPointToPath(x, y);
|
|
||||||
const lassoPath = super
|
|
||||||
.getCurrentTrail()
|
|
||||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
|
|
||||||
if (lassoPath) {
|
|
||||||
// TODO: further OPT: do not check elements that are "far away"
|
|
||||||
const elementsMap = this.app.scene.getNonDeletedElementsMap();
|
|
||||||
const closedPath = polygonFromPoints(lassoPath);
|
|
||||||
// need to clear the enclosed elements as path might change
|
|
||||||
this.enclosedElements.clear();
|
|
||||||
for (const [, element] of elementsMap) {
|
|
||||||
if (!this.intersectedElements.has(element.id)) {
|
|
||||||
const intersects = intersectionTest(lassoPath, element, elementsMap);
|
|
||||||
if (intersects) {
|
|
||||||
this.intersectedElements.add(element.id);
|
|
||||||
} else {
|
|
||||||
// TODO: check bounding box is at least in the lasso path area first
|
|
||||||
// BUT: need to compare bounding box check with enclosure check performance
|
|
||||||
const enclosed = enclosureTest(closedPath, element, elementsMap);
|
|
||||||
if (enclosed) {
|
|
||||||
this.enclosedElements.add(element.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
intersectedElementIds: this.intersectedElements,
|
|
||||||
enclosedElementIds: this.enclosedElements,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
endPath(): void {
|
|
||||||
super.endPath();
|
|
||||||
super.clearTrails();
|
|
||||||
this.intersectedElements.clear();
|
|
||||||
this.enclosedElements.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,6 @@ import {
|
|||||||
type Radians,
|
type Radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
import { renderLassoSelection } from "../lasso";
|
|
||||||
|
|
||||||
const renderElbowArrowMidPointHighlight = (
|
const renderElbowArrowMidPointHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getElementAbsoluteCoords } from "../element";
|
|||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getTargetFrame,
|
getTargetFrame,
|
||||||
shouldApplyFrameClip,
|
isElementInFrame,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import {
|
import {
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
@@ -273,8 +273,6 @@ const _renderStaticScene = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const inFrameGroupsMap = new Map<string, boolean>();
|
|
||||||
|
|
||||||
// Paint visible elements
|
// Paint visible elements
|
||||||
visibleElements
|
visibleElements
|
||||||
.filter((el) => !isIframeLikeElement(el))
|
.filter((el) => !isIframeLikeElement(el))
|
||||||
@@ -299,16 +297,9 @@ const _renderStaticScene = ({
|
|||||||
appState.frameRendering.clip
|
appState.frameRendering.clip
|
||||||
) {
|
) {
|
||||||
const frame = getTargetFrame(element, elementsMap, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
if (
|
|
||||||
frame &&
|
// TODO do we need to check isElementInFrame here?
|
||||||
shouldApplyFrameClip(
|
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||||
element,
|
|
||||||
frame,
|
|
||||||
appState,
|
|
||||||
elementsMap,
|
|
||||||
inFrameGroupsMap,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
renderElement(
|
renderElement(
|
||||||
@@ -409,16 +400,7 @@ const _renderStaticScene = ({
|
|||||||
|
|
||||||
const frame = getTargetFrame(element, elementsMap, appState);
|
const frame = getTargetFrame(element, elementsMap, appState);
|
||||||
|
|
||||||
if (
|
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||||
frame &&
|
|
||||||
shouldApplyFrameClip(
|
|
||||||
element,
|
|
||||||
frame,
|
|
||||||
appState,
|
|
||||||
elementsMap,
|
|
||||||
inFrameGroupsMap,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
frameClip(frame, context, renderConfig, appState);
|
frameClip(frame, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -183,12 +183,10 @@ export const getSelectedElements = (
|
|||||||
includeElementsInFrames?: boolean;
|
includeElementsInFrames?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const addedElements = new Set<ExcalidrawElement["id"]>();
|
|
||||||
const selectedElements: ExcalidrawElement[] = [];
|
const selectedElements: ExcalidrawElement[] = [];
|
||||||
for (const element of elements.values()) {
|
for (const element of elements.values()) {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
selectedElements.push(element);
|
selectedElements.push(element);
|
||||||
addedElements.add(element.id);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -197,7 +195,6 @@ export const getSelectedElements = (
|
|||||||
appState.selectedElementIds[element?.containerId]
|
appState.selectedElementIds[element?.containerId]
|
||||||
) {
|
) {
|
||||||
selectedElements.push(element);
|
selectedElements.push(element);
|
||||||
addedElements.add(element.id);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +203,8 @@ export const getSelectedElements = (
|
|||||||
const elementsToInclude: ExcalidrawElement[] = [];
|
const elementsToInclude: ExcalidrawElement[] = [];
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
getFrameChildren(elements, element.id).forEach(
|
getFrameChildren(elements, element.id).forEach((e) =>
|
||||||
(e) => !addedElements.has(e.id) && elementsToInclude.push(e),
|
elementsToInclude.push(e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
elementsToInclude.push(element);
|
elementsToInclude.push(element);
|
||||||
|
|||||||
@@ -10896,7 +10896,12 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [],
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "6Rm4g567UQM4WjLwej2Vc",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@@ -10904,7 +10909,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"height": 126,
|
"height": 126,
|
||||||
"id": "KPrBI4g_v9qUB1XxYLgSz",
|
"id": "KPrBI4g_v9qUB1XxYLgSz",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": true,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@@ -10917,7 +10922,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"width": 157,
|
"width": 157,
|
||||||
"x": 600,
|
"x": 600,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -10928,7 +10933,12 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [],
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "6Rm4g567UQM4WjLwej2Vc",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@@ -10936,7 +10946,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"height": 129,
|
"height": 129,
|
||||||
"id": "u2JGnnmoJ0VATV4vCNJE5",
|
"id": "u2JGnnmoJ0VATV4vCNJE5",
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": true,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@@ -10949,7 +10959,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 6,
|
||||||
"width": 124,
|
"width": 124,
|
||||||
"x": 1152,
|
"x": 1152,
|
||||||
"y": 516,
|
"y": 516,
|
||||||
@@ -10973,15 +10983,15 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"focus": "-0.00161",
|
"focus": "-0.00161",
|
||||||
"gap": "3.53708",
|
"gap": "3.53708",
|
||||||
},
|
},
|
||||||
"endIsSpecial": false,
|
"endIsSpecial": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fixedSegments": [],
|
"fixedSegments": null,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "236.10000",
|
"height": "448.10100",
|
||||||
"id": "6Rm4g567UQM4WjLwej2Vc",
|
"id": "6Rm4g567UQM4WjLwej2Vc",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": true,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
@@ -10992,12 +11002,16 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"178.90000",
|
"225.95000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"178.90000",
|
"225.95000",
|
||||||
"236.10000",
|
"448.10100",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"451.90000",
|
||||||
|
"448.10100",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -11014,16 +11028,16 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"focus": "-0.00159",
|
"focus": "-0.00159",
|
||||||
"gap": 5,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"startIsSpecial": false,
|
"startIsSpecial": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 6,
|
||||||
"width": "178.90000",
|
"width": "451.90000",
|
||||||
"x": 1035,
|
"x": 762,
|
||||||
"y": "274.90000",
|
"y": "62.90000",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -11035,7 +11049,8 @@ History {
|
|||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"redoStack": [
|
"redoStack": [],
|
||||||
|
"undoStack": [
|
||||||
HistoryEntry {
|
HistoryEntry {
|
||||||
"appStateChange": AppStateChange {
|
"appStateChange": AppStateChange {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
@@ -11044,12 +11059,86 @@ History {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elementsChange": ElementsChange {
|
"elementsChange": ElementsChange {
|
||||||
"added": Map {
|
"added": Map {},
|
||||||
"6Rm4g567UQM4WjLwej2Vc" => Delta {
|
"removed": Map {
|
||||||
|
"KPrBI4g_v9qUB1XxYLgSz" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"isDeleted": true,
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 126,
|
||||||
|
"index": "a0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": {
|
||||||
|
"type": 3,
|
||||||
|
},
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"width": 157,
|
||||||
|
"x": 873,
|
||||||
|
"y": 212,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"isDeleted": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"u2JGnnmoJ0VATV4vCNJE5" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 129,
|
||||||
|
"index": "a1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": {
|
||||||
|
"type": 3,
|
||||||
|
},
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "diamond",
|
||||||
|
"width": 124,
|
||||||
|
"x": 1152,
|
||||||
|
"y": 516,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"isDeleted": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"updated": Map {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HistoryEntry {
|
||||||
|
"appStateChange": AppStateChange {
|
||||||
|
"delta": Delta {
|
||||||
|
"deleted": {},
|
||||||
|
"inserted": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"elementsChange": ElementsChange {
|
||||||
|
"added": Map {},
|
||||||
|
"removed": Map {
|
||||||
|
"6Rm4g567UQM4WjLwej2Vc" => Delta {
|
||||||
|
"deleted": {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
@@ -11114,15 +11203,14 @@ History {
|
|||||||
"x": 1035,
|
"x": 1035,
|
||||||
"y": "274.90000",
|
"y": "274.90000",
|
||||||
},
|
},
|
||||||
|
"inserted": {
|
||||||
|
"isDeleted": true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"removed": Map {},
|
|
||||||
"updated": Map {
|
"updated": Map {
|
||||||
"KPrBI4g_v9qUB1XxYLgSz" => Delta {
|
"KPrBI4g_v9qUB1XxYLgSz" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "6Rm4g567UQM4WjLwej2Vc",
|
"id": "6Rm4g567UQM4WjLwej2Vc",
|
||||||
@@ -11130,12 +11218,12 @@ History {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"inserted": {
|
||||||
|
"boundElements": [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"u2JGnnmoJ0VATV4vCNJE5" => Delta {
|
"u2JGnnmoJ0VATV4vCNJE5" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "6Rm4g567UQM4WjLwej2Vc",
|
"id": "6Rm4g567UQM4WjLwej2Vc",
|
||||||
@@ -11143,88 +11231,14 @@ History {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
HistoryEntry {
|
|
||||||
"appStateChange": AppStateChange {
|
|
||||||
"delta": Delta {
|
|
||||||
"deleted": {},
|
|
||||||
"inserted": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"elementsChange": ElementsChange {
|
|
||||||
"added": Map {
|
|
||||||
"KPrBI4g_v9qUB1XxYLgSz" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"isDeleted": true,
|
|
||||||
},
|
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"angle": 0,
|
"boundElements": [],
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 126,
|
|
||||||
"index": "a0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"width": 157,
|
|
||||||
"x": 600,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"u2JGnnmoJ0VATV4vCNJE5" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"isDeleted": true,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 129,
|
|
||||||
"index": "a1",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "diamond",
|
|
||||||
"width": 124,
|
|
||||||
"x": 1152,
|
|
||||||
"y": 516,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"removed": Map {},
|
|
||||||
"updated": Map {},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"undoStack": [],
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1051383431,
|
"versionNonce": 1996028265,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": 50,
|
"y": 50,
|
||||||
|
|||||||
@@ -2077,15 +2077,16 @@ describe("history", () => {
|
|||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.redo();
|
||||||
|
|
||||||
const modifiedArrow = h.elements.filter(
|
const modifiedArrow = h.elements.filter(
|
||||||
(el) => el.type === "arrow",
|
(el) => el.type === "arrow",
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
expect(modifiedArrow.points).toCloselyEqualPoints([
|
expect(modifiedArrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[178.9, 0],
|
[225.95000000000005, 0],
|
||||||
[178.9, 236.1],
|
[225.95000000000005, 448.10100010002003],
|
||||||
|
[451.9000000000001, 448.10100010002003],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
|||||||
|
|
||||||
export type ToolType =
|
export type ToolType =
|
||||||
| "selection"
|
| "selection"
|
||||||
| "lassoSelection"
|
|
||||||
| "rectangle"
|
| "rectangle"
|
||||||
| "diamond"
|
| "diamond"
|
||||||
| "ellipse"
|
| "ellipse"
|
||||||
@@ -409,8 +408,6 @@ export interface AppState {
|
|||||||
croppingElementId: ExcalidrawElement["id"] | null;
|
croppingElementId: ExcalidrawElement["id"] | null;
|
||||||
|
|
||||||
searchMatches: readonly SearchMatch[];
|
searchMatches: readonly SearchMatch[];
|
||||||
|
|
||||||
lassoSelectionEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchMatch = {
|
type SearchMatch = {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||||
points: Point[],
|
points: Point[],
|
||||||
): Polyline<Point> => {
|
): Polyline<Point> => {
|
||||||
let previousPoint: Point = points[0];
|
let previousPoint: Point = points[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user