mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
merge with master
This commit is contained in:
commit
e8e97adace
86 changed files with 4355 additions and 1113 deletions
|
@ -21,6 +21,7 @@ import {
|
|||
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isTextBindableContainer,
|
||||
isTextElement,
|
||||
isUsingAdaptiveRadius,
|
||||
|
@ -46,6 +47,8 @@ import { CaptureUpdateAction } from "../store";
|
|||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
export const actionUnbindText = register({
|
||||
|
@ -155,6 +158,7 @@ export const actionBindText = register({
|
|||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||
});
|
||||
mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
|
@ -226,8 +230,8 @@ export const actionWrapTextInContainer = register({
|
|||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||
return selectedElements.length > 0 && areTextElements;
|
||||
const someTextElements = selectedElements.some((el) => isTextElement(el));
|
||||
return selectedElements.length > 0 && someTextElements;
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
|
|
@ -29,6 +29,7 @@ import { ToolButton } from "../components/ToolButton";
|
|||
import { Tooltip } from "../components/Tooltip";
|
||||
import {
|
||||
handIcon,
|
||||
LassoIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
TrashIcon,
|
||||
|
@ -52,7 +53,6 @@ import type { AppState, Offsets } from "../types";
|
|||
export const actionChangeViewBackgroundColor = register({
|
||||
name: "changeViewBackgroundColor",
|
||||
label: "labels.canvasBackground",
|
||||
paletteName: "Change canvas background color",
|
||||
trackEvent: false,
|
||||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
|
@ -90,7 +90,6 @@ export const actionChangeViewBackgroundColor = register({
|
|||
export const actionClearCanvas = register({
|
||||
name: "clearCanvas",
|
||||
label: "labels.clearCanvas",
|
||||
paletteName: "Clear canvas",
|
||||
icon: TrashIcon,
|
||||
trackEvent: { category: "canvas" },
|
||||
predicate: (elements, appState, props, app) => {
|
||||
|
@ -525,10 +524,42 @@ export const actionToggleEraserTool = register({
|
|||
keyTest: (event) => event.key === KEYS.E,
|
||||
});
|
||||
|
||||
export const actionToggleLassoTool = register({
|
||||
name: "toggleLassoTool",
|
||||
label: "toolBar.lasso",
|
||||
icon: LassoIcon,
|
||||
trackEvent: { category: "toolbar" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
let activeTool: AppState["activeTool"];
|
||||
|
||||
if (appState.activeTool.type !== "lasso") {
|
||||
activeTool = updateActiveTool(appState, {
|
||||
type: "lasso",
|
||||
fromSelection: false,
|
||||
});
|
||||
setCursor(app.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
|
||||
} else {
|
||||
activeTool = updateActiveTool(appState, {
|
||||
type: "selection",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
activeEmbeddable: null,
|
||||
activeTool,
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const actionToggleHandTool = register({
|
||||
name: "toggleHandTool",
|
||||
label: "toolBar.hand",
|
||||
paletteName: "Toggle hand tool",
|
||||
trackEvent: { category: "toolbar" },
|
||||
icon: handIcon,
|
||||
viewMode: false,
|
||||
|
|
|
@ -7,26 +7,17 @@ import {
|
|||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectedElements,
|
||||
getSelectionStateForElements,
|
||||
} from "@excalidraw/element/selection";
|
||||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { DuplicateIcon } from "../components/icons";
|
||||
|
||||
|
@ -65,52 +56,49 @@ export const actionDuplicateSelection = register({
|
|||
}
|
||||
}
|
||||
|
||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
),
|
||||
appState,
|
||||
randomizeSeed: true,
|
||||
overrides: (element) => ({
|
||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
reverseOrder: false,
|
||||
});
|
||||
),
|
||||
appState,
|
||||
randomizeSeed: true,
|
||||
overrides: ({ origElement, origIdToDuplicateId }) => {
|
||||
const duplicateFrameId =
|
||||
origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
|
||||
return {
|
||||
x: origElement.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: origElement.y + DEFAULT_GRID_SIZE / 2,
|
||||
frameId: duplicateFrameId ?? origElement.frameId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (app.props.onDuplicate && nextElements) {
|
||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
||||
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||
const mappedElements = app.props.onDuplicate(
|
||||
elementsWithDuplicates,
|
||||
elements,
|
||||
);
|
||||
if (mappedElements) {
|
||||
nextElements = mappedElements;
|
||||
elementsWithDuplicates = mappedElements;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
||||
elements: syncMovedIndices(
|
||||
elementsWithDuplicates,
|
||||
arrayToMap(duplicatedElements),
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...updateLinearElementEditors(duplicatedElements),
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||
duplicatedElements,
|
||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
getNonDeletedElements(nextElements),
|
||||
...getSelectionStateForElements(
|
||||
duplicatedElements,
|
||||
getNonDeletedElements(elementsWithDuplicates),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
@ -130,24 +118,3 @@ export const actionDuplicateSelection = register({
|
|||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
|
||||
const linears = clonedElements.filter(isLinearElement);
|
||||
if (linears.length === 1) {
|
||||
const linear = linears[0];
|
||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||
const onlySingleLinearSelected = clonedElements.every(
|
||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||
);
|
||||
|
||||
if (onlySingleLinearSelected) {
|
||||
return {
|
||||
selectedLinearElement: new LinearElementEditor(linear),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedLinearElement: null,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -90,7 +90,6 @@ export const actionToggleElementLock = register({
|
|||
|
||||
export const actionUnlockAllElements = register({
|
||||
name: "unlockAllElements",
|
||||
paletteName: "Unlock all elements",
|
||||
trackEvent: { category: "canvas" },
|
||||
viewMode: false,
|
||||
icon: UnlockedIcon,
|
||||
|
|
|
@ -9,7 +9,6 @@ export const actionToggleStats = register({
|
|||
name: "stats",
|
||||
label: "stats.fullTitle",
|
||||
icon: abacusIcon,
|
||||
paletteName: "Toggle stats",
|
||||
viewMode: true,
|
||||
trackEvent: { category: "menu" },
|
||||
keywords: ["edit", "attributes", "customize"],
|
||||
|
|
|
@ -8,7 +8,6 @@ import { register } from "./register";
|
|||
export const actionToggleViewMode = register({
|
||||
name: "viewMode",
|
||||
label: "labels.viewMode",
|
||||
paletteName: "Toggle view mode",
|
||||
icon: eyeIcon,
|
||||
viewMode: true,
|
||||
trackEvent: {
|
||||
|
|
|
@ -9,7 +9,6 @@ export const actionToggleZenMode = register({
|
|||
name: "zenMode",
|
||||
label: "buttons.zenMode",
|
||||
icon: coffeeIcon,
|
||||
paletteName: "Toggle zen mode",
|
||||
viewMode: true,
|
||||
trackEvent: {
|
||||
category: "canvas",
|
||||
|
|
|
@ -140,6 +140,7 @@ export type ActionName =
|
|||
| "linkToElement"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool"
|
||||
| "toggleShapeSwitch";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
|
|
|
@ -23,6 +23,8 @@ export interface Trail {
|
|||
|
||||
export interface AnimatedTrailOptions {
|
||||
fill: (trail: AnimatedTrail) => string;
|
||||
stroke?: (trail: AnimatedTrail) => string;
|
||||
animateTrail?: boolean;
|
||||
}
|
||||
|
||||
export class AnimatedTrail implements Trail {
|
||||
|
@ -31,16 +33,28 @@ export class AnimatedTrail implements Trail {
|
|||
|
||||
private container?: SVGSVGElement;
|
||||
private trailElement: SVGPathElement;
|
||||
private trailAnimation?: SVGAnimateElement;
|
||||
|
||||
constructor(
|
||||
private animationFrameHandler: AnimationFrameHandler,
|
||||
private app: App,
|
||||
protected app: App,
|
||||
private options: Partial<LaserPointerOptions> &
|
||||
Partial<AnimatedTrailOptions>,
|
||||
) {
|
||||
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
||||
|
||||
this.trailElement = document.createElementNS(SVG_NS, "path");
|
||||
if (this.options.animateTrail) {
|
||||
this.trailAnimation = document.createElementNS(SVG_NS, "animate");
|
||||
// TODO: make this configurable
|
||||
this.trailAnimation.setAttribute("attributeName", "stroke-dashoffset");
|
||||
this.trailElement.setAttribute("stroke-dasharray", "7 7");
|
||||
this.trailElement.setAttribute("stroke-dashoffset", "10");
|
||||
this.trailAnimation.setAttribute("from", "0");
|
||||
this.trailAnimation.setAttribute("to", `-14`);
|
||||
this.trailAnimation.setAttribute("dur", "0.3s");
|
||||
this.trailElement.appendChild(this.trailAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
get hasCurrentTrail() {
|
||||
|
@ -104,8 +118,23 @@ export class AnimatedTrail implements Trail {
|
|||
}
|
||||
}
|
||||
|
||||
getCurrentTrail() {
|
||||
return this.currentTrail;
|
||||
}
|
||||
|
||||
clearTrails() {
|
||||
this.pastTrails = [];
|
||||
this.currentTrail = undefined;
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.pastTrails = [];
|
||||
this.start();
|
||||
if (this.trailAnimation) {
|
||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||
this.trailAnimation.setAttribute("repeatCount", "indefinite");
|
||||
}
|
||||
}
|
||||
|
||||
private onFrame() {
|
||||
|
@ -132,14 +161,25 @@ export class AnimatedTrail implements Trail {
|
|||
const svgPaths = paths.join(" ").trim();
|
||||
|
||||
this.trailElement.setAttribute("d", svgPaths);
|
||||
this.trailElement.setAttribute(
|
||||
"fill",
|
||||
(this.options.fill ?? (() => "black"))(this),
|
||||
);
|
||||
if (this.trailAnimation) {
|
||||
this.trailElement.setAttribute(
|
||||
"fill",
|
||||
(this.options.fill ?? (() => "black"))(this),
|
||||
);
|
||||
this.trailElement.setAttribute(
|
||||
"stroke",
|
||||
(this.options.stroke ?? (() => "black"))(this),
|
||||
);
|
||||
} else {
|
||||
this.trailElement.setAttribute(
|
||||
"fill",
|
||||
(this.options.fill ?? (() => "black"))(this),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private drawTrail(trail: LaserPointer, state: AppState): string {
|
||||
const stroke = trail
|
||||
const _stroke = trail
|
||||
.getStrokeOutline(trail.options.size / state.zoom.value)
|
||||
.map(([x, y]) => {
|
||||
const result = sceneCoordsToViewportCoords(
|
||||
|
@ -150,6 +190,10 @@ export class AnimatedTrail implements Trail {
|
|||
return [result.x, result.y];
|
||||
});
|
||||
|
||||
const stroke = this.trailAnimation
|
||||
? _stroke.slice(0, _stroke.length / 2)
|
||||
: _stroke;
|
||||
|
||||
return getSvgPathFromStroke(stroke, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ export const getDefaultAppState = (): Omit<
|
|||
type: "selection",
|
||||
customType: null,
|
||||
locked: DEFAULT_ELEMENT_PROPS.locked,
|
||||
fromSelection: false,
|
||||
lastActiveTool: null,
|
||||
},
|
||||
penMode: false,
|
||||
|
|
|
@ -62,6 +62,7 @@ import {
|
|||
mermaidLogoIcon,
|
||||
laserPointerToolIcon,
|
||||
MagicIcon,
|
||||
LassoIcon,
|
||||
} from "./icons";
|
||||
|
||||
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||
|
@ -83,7 +84,6 @@ export const canChangeStrokeColor = (
|
|||
|
||||
return (
|
||||
(hasStrokeColor(appState.activeTool.type) &&
|
||||
appState.activeTool.type !== "image" &&
|
||||
commonSelectedType !== "image" &&
|
||||
commonSelectedType !== "frame" &&
|
||||
commonSelectedType !== "magicframe") ||
|
||||
|
@ -295,6 +295,8 @@ export const ShapesSwitcher = ({
|
|||
|
||||
const frameToolSelected = activeTool.type === "frame";
|
||||
const laserToolSelected = activeTool.type === "laser";
|
||||
const lassoToolSelected = activeTool.type === "lasso";
|
||||
|
||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||
|
||||
const { TTDDialogTriggerTunnel } = useTunnels();
|
||||
|
@ -316,6 +318,7 @@ export const ShapesSwitcher = ({
|
|||
const shortcut = letter
|
||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable })}
|
||||
|
@ -333,6 +336,14 @@ export const ShapesSwitcher = ({
|
|||
if (!appState.penDetected && pointerType === "pen") {
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
|
||||
if (value === "selection") {
|
||||
if (appState.activeTool.type === "selection") {
|
||||
app.setActiveTool({ type: "lasso" });
|
||||
} else {
|
||||
app.setActiveTool({ type: "selection" });
|
||||
}
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
if (appState.activeTool.type !== value) {
|
||||
|
@ -358,6 +369,7 @@ export const ShapesSwitcher = ({
|
|||
"App-toolbar__extra-tools-trigger--selected":
|
||||
frameToolSelected ||
|
||||
embeddableToolSelected ||
|
||||
lassoToolSelected ||
|
||||
// in collab we're already highlighting the laser button
|
||||
// outside toolbar, so let's not highlight extra-tools button
|
||||
// on top of it
|
||||
|
@ -366,7 +378,15 @@ export const ShapesSwitcher = ({
|
|||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||
title={t("toolBar.extraTools")}
|
||||
>
|
||||
{extraToolsIcon}
|
||||
{frameToolSelected
|
||||
? frameToolIcon
|
||||
: embeddableToolSelected
|
||||
? EmbedIcon
|
||||
: laserToolSelected && !app.props.isCollaborating
|
||||
? laserPointerToolIcon
|
||||
: lassoToolSelected
|
||||
? LassoIcon
|
||||
: extraToolsIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||
|
@ -399,6 +419,14 @@ export const ShapesSwitcher = ({
|
|||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||
icon={LassoIcon}
|
||||
data-testid="toolbar-lasso"
|
||||
selected={lassoToolSelected}
|
||||
>
|
||||
{t("toolBar.lasso")}
|
||||
</DropdownMenu.Item>
|
||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||
Generate
|
||||
</div>
|
||||
|
|
|
@ -99,6 +99,7 @@ import {
|
|||
isShallowEqual,
|
||||
arrayToMap,
|
||||
type EXPORT_IMAGE_TYPES,
|
||||
randomInteger,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
|
@ -278,6 +279,7 @@ import {
|
|||
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectionStateForElements,
|
||||
makeNextSelectedElementIds,
|
||||
} from "@excalidraw/element/selection";
|
||||
|
||||
|
@ -453,7 +455,6 @@ import {
|
|||
import { Emitter } from "../emitter";
|
||||
import { ElementCanvasButtons } from "../components/ElementCanvasButtons";
|
||||
import { Store, CaptureUpdateAction } from "../store";
|
||||
import { AnimatedTrail } from "../animated-trail";
|
||||
import { LaserTrails } from "../laser-trails";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { textWysiwyg } from "../wysiwyg/textWysiwyg";
|
||||
|
@ -461,6 +462,10 @@ import { isOverScrollBars } from "../scene/scrollbars";
|
|||
|
||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||
|
||||
import { LassoTrail } from "../lasso";
|
||||
|
||||
import { EraserTrail } from "../eraser";
|
||||
|
||||
import ShapeSwitch, {
|
||||
getSwitchableTypeFromElements,
|
||||
shapeSwitchAtom,
|
||||
|
@ -677,26 +682,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
animationFrameHandler = new AnimationFrameHandler();
|
||||
|
||||
laserTrails = new LaserTrails(this.animationFrameHandler, this);
|
||||
eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, {
|
||||
streamline: 0.2,
|
||||
size: 5,
|
||||
keepHead: true,
|
||||
sizeMapping: (c) => {
|
||||
const DECAY_TIME = 200;
|
||||
const DECAY_LENGTH = 10;
|
||||
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: () =>
|
||||
this.state.theme === THEME.LIGHT
|
||||
? "rgba(0, 0, 0, 0.2)"
|
||||
: "rgba(255, 255, 255, 0.2)",
|
||||
});
|
||||
eraserTrail = new EraserTrail(this.animationFrameHandler, this);
|
||||
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
|
||||
|
||||
onChangeEmitter = new Emitter<
|
||||
[
|
||||
|
@ -1675,7 +1662,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
<div className="excalidraw-contextMenuContainer" />
|
||||
<div className="excalidraw-eye-dropper-container" />
|
||||
<SVGLayer
|
||||
trails={[this.laserTrails, this.eraserTrail]}
|
||||
trails={[
|
||||
this.laserTrails,
|
||||
this.lassoTrail,
|
||||
this.eraserTrail,
|
||||
]}
|
||||
/>
|
||||
{selectedElements.length === 1 &&
|
||||
this.state.openDialog?.name !==
|
||||
|
@ -1844,6 +1835,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
scale={window.devicePixelRatio}
|
||||
appState={this.state}
|
||||
renderScrollbars={
|
||||
this.props.renderScrollbars === true
|
||||
}
|
||||
device={this.device}
|
||||
renderInteractiveSceneCallback={
|
||||
this.renderInteractiveSceneCallback
|
||||
|
@ -3283,7 +3277,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||
|
||||
const { newElements } = duplicateElements({
|
||||
const { duplicatedElements } = duplicateElements({
|
||||
type: "everything",
|
||||
elements: elements.map((element) => {
|
||||
return newElementWith(element, {
|
||||
|
@ -3295,7 +3289,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
|
||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = [...prevElements, ...newElements];
|
||||
let nextElements = [...prevElements, ...duplicatedElements];
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
nextElements,
|
||||
|
@ -3304,13 +3298,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
nextElements = mappedNewSceneElements || nextElements;
|
||||
|
||||
syncMovedIndices(nextElements, arrayToMap(newElements));
|
||||
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
if (topLayerFrame) {
|
||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||
newElements,
|
||||
duplicatedElements,
|
||||
topLayerFrame,
|
||||
);
|
||||
addElementsToFrame(
|
||||
|
@ -3323,7 +3317,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
|
||||
newElements.forEach((newElement) => {
|
||||
duplicatedElements.forEach((newElement) => {
|
||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||
const container = getContainerElement(
|
||||
newElement,
|
||||
|
@ -3339,7 +3333,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
||||
if (isSafari) {
|
||||
Fonts.loadElementsFonts(newElements).then((fontFaces) => {
|
||||
Fonts.loadElementsFonts(duplicatedElements).then((fontFaces) => {
|
||||
this.fonts.onLoaded(fontFaces);
|
||||
});
|
||||
}
|
||||
|
@ -3351,7 +3345,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.store.shouldCaptureIncrement();
|
||||
|
||||
const nextElementsToSelect =
|
||||
excludeElementsInFramesFromSelection(newElements);
|
||||
excludeElementsInFramesFromSelection(duplicatedElements);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
|
@ -3394,7 +3388,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setActiveTool({ type: "selection" });
|
||||
|
||||
if (opts.fitToContent) {
|
||||
this.scrollToContent(newElements, {
|
||||
this.scrollToContent(duplicatedElements, {
|
||||
fitToContent: true,
|
||||
canvasOffsets: this.getEditorUIOffsets(),
|
||||
});
|
||||
|
@ -4666,7 +4660,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.openDialog?.name === "elementLinkSelector"
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
} else if (this.state.activeTool.type === "selection") {
|
||||
} else if (
|
||||
this.state.activeTool.type === "selection" ||
|
||||
this.state.activeTool.type === "lasso"
|
||||
) {
|
||||
resetCursor(this.interactiveCanvas);
|
||||
} else {
|
||||
setCursorForShape(this.interactiveCanvas, this.state);
|
||||
|
@ -4774,7 +4771,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
)
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) & { locked?: boolean; fromSelection?: boolean },
|
||||
keepSelection = false,
|
||||
) => {
|
||||
if (!this.isToolSupported(tool.type)) {
|
||||
console.warn(
|
||||
|
@ -4816,7 +4814,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
|
||||
if (nextActiveTool.type !== "selection") {
|
||||
if (nextActiveTool.type === "lasso") {
|
||||
return {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
...(keepSelection
|
||||
? {}
|
||||
: {
|
||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
|
||||
editingGroupId: null,
|
||||
multiElement: null,
|
||||
}),
|
||||
...commonResets,
|
||||
};
|
||||
} else if (nextActiveTool.type !== "selection") {
|
||||
return {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
|
@ -5173,7 +5185,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return elements;
|
||||
}
|
||||
|
||||
private getElementHitThreshold() {
|
||||
getElementHitThreshold() {
|
||||
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
||||
}
|
||||
|
||||
|
@ -5362,37 +5374,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y: sceneY,
|
||||
});
|
||||
|
||||
const element = existingTextElement
|
||||
? existingTextElement
|
||||
: newTextElement({
|
||||
x: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterX
|
||||
: sceneX,
|
||||
y: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterY
|
||||
: sceneY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
textAlign: parentCenterPosition
|
||||
? "center"
|
||||
: this.state.currentItemTextAlign,
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
angle: container?.angle ?? (0 as Radians),
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
const element =
|
||||
existingTextElement ||
|
||||
newTextElement({
|
||||
x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX,
|
||||
y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
textAlign: parentCenterPosition
|
||||
? "center"
|
||||
: this.state.currentItemTextAlign,
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
angle: container
|
||||
? isArrowElement(container)
|
||||
? (0 as Radians)
|
||||
: container.angle
|
||||
: (0 as Radians),
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
|
||||
if (!existingTextElement && shouldBindToContainer && container) {
|
||||
mutateElement(container, {
|
||||
|
@ -6229,101 +6241,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private handleEraser = (
|
||||
event: PointerEvent,
|
||||
pointerDownState: PointerDownState,
|
||||
scenePointer: { x: number; y: number },
|
||||
) => {
|
||||
this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y);
|
||||
|
||||
let didChange = false;
|
||||
|
||||
const processedGroups = new Set<ExcalidrawElement["id"]>();
|
||||
const nonDeletedElements = this.scene.getNonDeletedElements();
|
||||
|
||||
const processElements = (elements: ExcalidrawElement[]) => {
|
||||
for (const element of elements) {
|
||||
if (element.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
if (this.elementsPendingErasure.delete(element.id)) {
|
||||
didChange = true;
|
||||
}
|
||||
} else if (!this.elementsPendingErasure.has(element.id)) {
|
||||
didChange = true;
|
||||
this.elementsPendingErasure.add(element.id);
|
||||
}
|
||||
|
||||
// (un)erase groups atomically
|
||||
if (didChange && element.groupIds?.length) {
|
||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||
if (!processedGroups.has(shallowestGroupId)) {
|
||||
processedGroups.add(shallowestGroupId);
|
||||
const elems = getElementsInGroup(
|
||||
nonDeletedElements,
|
||||
shallowestGroupId,
|
||||
);
|
||||
for (const elem of elems) {
|
||||
if (event.altKey) {
|
||||
this.elementsPendingErasure.delete(elem.id);
|
||||
} else {
|
||||
this.elementsPendingErasure.add(elem.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const distance = pointDistance(
|
||||
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
const elementsToErase = this.eraserTrail.addPointToPath(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
event.altKey,
|
||||
);
|
||||
const threshold = this.getElementHitThreshold();
|
||||
const p = { ...pointerDownState.lastCoords };
|
||||
let samplingInterval = 0;
|
||||
while (samplingInterval <= distance) {
|
||||
const hitElements = this.getElementsAtPosition(p.x, p.y);
|
||||
processElements(hitElements);
|
||||
|
||||
// Exit since we reached current point
|
||||
if (samplingInterval === distance) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate next point in the line at a distance of sampling interval
|
||||
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
||||
|
||||
const distanceRatio = samplingInterval / distance;
|
||||
const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x;
|
||||
const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y;
|
||||
p.x = nextX;
|
||||
p.y = nextY;
|
||||
}
|
||||
|
||||
pointerDownState.lastCoords.x = scenePointer.x;
|
||||
pointerDownState.lastCoords.y = scenePointer.y;
|
||||
|
||||
if (didChange) {
|
||||
for (const element of this.scene.getNonDeletedElements()) {
|
||||
if (
|
||||
isBoundToContainer(element) &&
|
||||
(this.elementsPendingErasure.has(element.id) ||
|
||||
this.elementsPendingErasure.has(element.containerId))
|
||||
) {
|
||||
if (event.altKey) {
|
||||
this.elementsPendingErasure.delete(element.id);
|
||||
this.elementsPendingErasure.delete(element.containerId);
|
||||
} else {
|
||||
this.elementsPendingErasure.add(element.id);
|
||||
this.elementsPendingErasure.add(element.containerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
|
||||
this.triggerRender();
|
||||
}
|
||||
this.elementsPendingErasure = new Set(elementsToErase);
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
// set touch moving for mobile context menu
|
||||
|
@ -6643,6 +6570,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
!this.state.penMode ||
|
||||
event.pointerType !== "touch" ||
|
||||
this.state.activeTool.type === "selection" ||
|
||||
this.state.activeTool.type === "lasso" ||
|
||||
this.state.activeTool.type === "text" ||
|
||||
this.state.activeTool.type === "image";
|
||||
|
||||
|
@ -6650,7 +6578,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeTool.type === "text") {
|
||||
if (this.state.activeTool.type === "lasso") {
|
||||
this.lassoTrail.startPath(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else if (this.state.activeTool.type === "text") {
|
||||
this.handleTextOnPointerDown(event, pointerDownState);
|
||||
} else if (
|
||||
this.state.activeTool.type === "arrow" ||
|
||||
|
@ -7052,6 +6986,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
drag: {
|
||||
hasOccurred: false,
|
||||
offset: null,
|
||||
origin: { ...origin },
|
||||
},
|
||||
eventListeners: {
|
||||
onMove: null,
|
||||
|
@ -7107,7 +7042,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
private clearSelectionIfNotUsingSelection = (): void => {
|
||||
if (this.state.activeTool.type !== "selection") {
|
||||
if (
|
||||
this.state.activeTool.type !== "selection" &&
|
||||
this.state.activeTool.type !== "lasso"
|
||||
) {
|
||||
this.setState({
|
||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||
selectedGroupIds: {},
|
||||
|
@ -8163,7 +8101,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (isEraserActive(this.state)) {
|
||||
this.handleEraser(event, pointerDownState, pointerCoords);
|
||||
this.handleEraser(event, pointerCoords);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -8307,7 +8245,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (
|
||||
(hasHitASelectedElement ||
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||
!isSelectingPointsInLineEditor
|
||||
!isSelectingPointsInLineEditor &&
|
||||
this.state.activeTool.type !== "lasso"
|
||||
) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
|
||||
|
@ -8342,8 +8281,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.activeEmbeddable?.state !== "active"
|
||||
) {
|
||||
const dragOffset = {
|
||||
x: pointerCoords.x - pointerDownState.origin.x,
|
||||
y: pointerCoords.y - pointerDownState.origin.y,
|
||||
x: pointerCoords.x - pointerDownState.drag.origin.x,
|
||||
y: pointerCoords.y - pointerDownState.drag.origin.y,
|
||||
};
|
||||
|
||||
const originalElements = [
|
||||
|
@ -8525,51 +8464,125 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
if (
|
||||
hitElement &&
|
||||
// hit element may not end up being selected
|
||||
// if we're alt-dragging a common bounding box
|
||||
// over the hit element
|
||||
pointerDownState.hit.wasAddedToSelection &&
|
||||
!selectedElements.find((el) => el.id === hitElement.id)
|
||||
) {
|
||||
selectedElements.push(hitElement);
|
||||
}
|
||||
|
||||
const { newElements: clonedElements, elementsWithClones } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate: new Map(
|
||||
selectedElements.map((el) => [el.id, el]),
|
||||
),
|
||||
overrides: (el) => {
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
const idsOfElementsToDuplicate = new Map(
|
||||
selectedElements.map((el) => [el.id, el]),
|
||||
);
|
||||
|
||||
if (origEl) {
|
||||
return {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
};
|
||||
}
|
||||
const {
|
||||
duplicatedElements,
|
||||
duplicateElementsMap,
|
||||
elementsWithDuplicates,
|
||||
origIdToDuplicateId,
|
||||
} = duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate,
|
||||
overrides: ({ duplicateElement, origElement }) => {
|
||||
return {
|
||||
// reset to the original element's frameId (unless we've
|
||||
// duplicated alongside a frame in which case we need to
|
||||
// keep the duplicate frame's id) so that the element
|
||||
// frame membership is refreshed on pointerup
|
||||
// NOTE this is a hacky solution and should be done
|
||||
// differently
|
||||
frameId: duplicateElement.frameId ?? origElement.frameId,
|
||||
seed: randomInteger(),
|
||||
};
|
||||
},
|
||||
});
|
||||
duplicatedElements.forEach((element) => {
|
||||
pointerDownState.originalElements.set(
|
||||
element.id,
|
||||
deepCopyElement(element),
|
||||
);
|
||||
});
|
||||
|
||||
return {};
|
||||
},
|
||||
reverseOrder: true,
|
||||
});
|
||||
clonedElements.forEach((element) => {
|
||||
pointerDownState.originalElements.set(element.id, element);
|
||||
const mappedClonedElements = elementsWithDuplicates.map((el) => {
|
||||
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
|
||||
if (origEl) {
|
||||
return newElementWith(el, {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
elementsWithClones,
|
||||
mappedClonedElements,
|
||||
elements,
|
||||
);
|
||||
|
||||
const nextSceneElements = syncMovedIndices(
|
||||
mappedNewSceneElements || elementsWithClones,
|
||||
arrayToMap(clonedElements),
|
||||
const elementsWithIndices = syncMovedIndices(
|
||||
mappedNewSceneElements || mappedClonedElements,
|
||||
arrayToMap(duplicatedElements),
|
||||
);
|
||||
|
||||
this.scene.replaceAllElements(nextSceneElements);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||
// we need to update synchronously so as to keep pointerDownState,
|
||||
// appState, and scene elements in sync
|
||||
flushSync(() => {
|
||||
// swap hit element with the duplicated one
|
||||
if (pointerDownState.hit.element) {
|
||||
const cloneId = origIdToDuplicateId.get(
|
||||
pointerDownState.hit.element.id,
|
||||
);
|
||||
const clonedElement =
|
||||
cloneId && duplicateElementsMap.get(cloneId);
|
||||
pointerDownState.hit.element = clonedElement || null;
|
||||
}
|
||||
// swap hit elements with the duplicated ones
|
||||
pointerDownState.hit.allHitElements =
|
||||
pointerDownState.hit.allHitElements.reduce(
|
||||
(
|
||||
acc: typeof pointerDownState.hit.allHitElements,
|
||||
origHitElement,
|
||||
) => {
|
||||
const cloneId = origIdToDuplicateId.get(origHitElement.id);
|
||||
const clonedElement =
|
||||
cloneId && duplicateElementsMap.get(cloneId);
|
||||
if (clonedElement) {
|
||||
acc.push(clonedElement);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// update drag origin to the position at which we started
|
||||
// the duplication so that the drag offset is correct
|
||||
pointerDownState.drag.origin = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
);
|
||||
|
||||
// switch selected elements to the duplicated ones
|
||||
this.setState((prevState) => ({
|
||||
...getSelectionStateForElements(
|
||||
duplicatedElements,
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
),
|
||||
}));
|
||||
|
||||
this.scene.replaceAllElements(elementsWithIndices);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -8579,7 +8592,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (this.state.selectionElement) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
if (event.altKey) {
|
||||
this.setActiveTool(
|
||||
{ type: "lasso", fromSelection: true },
|
||||
event.shiftKey,
|
||||
);
|
||||
this.lassoTrail.startPath(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
this.setAppState({
|
||||
selectionElement: null,
|
||||
});
|
||||
} else {
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
}
|
||||
} else if (this.state.activeTool.type === "lasso") {
|
||||
if (!event.altKey && this.state.activeTool.fromSelection) {
|
||||
this.setActiveTool({ type: "selection" });
|
||||
this.createGenericElementOnPointerDown("selection", pointerDownState);
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
this.lassoTrail.endPath();
|
||||
} else {
|
||||
this.lassoTrail.addPointToPath(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// It is very important to read this.state within each move event,
|
||||
// otherwise we would read a stale one!
|
||||
|
@ -8784,7 +8827,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const x = event.clientX;
|
||||
const dx = x - pointerDownState.lastCoords.x;
|
||||
this.translateCanvas({
|
||||
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
||||
scrollX:
|
||||
this.state.scrollX -
|
||||
(dx * (currentScrollBars.horizontal?.deltaMultiplier || 1)) /
|
||||
this.state.zoom.value,
|
||||
});
|
||||
pointerDownState.lastCoords.x = x;
|
||||
return true;
|
||||
|
@ -8794,7 +8840,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const y = event.clientY;
|
||||
const dy = y - pointerDownState.lastCoords.y;
|
||||
this.translateCanvas({
|
||||
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
||||
scrollY:
|
||||
this.state.scrollY -
|
||||
(dy * (currentScrollBars.vertical?.deltaMultiplier || 1)) /
|
||||
this.state.zoom.value,
|
||||
});
|
||||
pointerDownState.lastCoords.y = y;
|
||||
return true;
|
||||
|
@ -8834,6 +8883,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
originSnapOffset: null,
|
||||
}));
|
||||
|
||||
// just in case, tool changes mid drag, always clean up
|
||||
this.lassoTrail.endPath();
|
||||
this.lastPointerMoveCoords = null;
|
||||
|
||||
SnapCache.setReferenceSnapPoints(null);
|
||||
|
@ -9550,6 +9601,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
// do not clear selection if lasso is active
|
||||
this.state.activeTool.type !== "lasso" &&
|
||||
// not elbow midpoint dragged
|
||||
!(hitElement && isElbowArrow(hitElement)) &&
|
||||
// not dragged
|
||||
|
@ -9648,7 +9701,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||
if (
|
||||
!activeTool.locked &&
|
||||
activeTool.type !== "freedraw" &&
|
||||
(activeTool.type !== "lasso" ||
|
||||
// if lasso is turned on but from selection => reset to selection
|
||||
(activeTool.type === "lasso" && activeTool.fromSelection))
|
||||
) {
|
||||
resetCursor(this.interactiveCanvas);
|
||||
this.setState({
|
||||
newElement: null,
|
||||
|
@ -10503,7 +10562,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
shouldResizeFromCenter: false,
|
||||
zoom: this.state.zoom.value,
|
||||
informMutation,
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
.color-picker-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8px 1.625rem;
|
||||
grid-template-columns: 1fr 20px 1.625rem;
|
||||
padding: 0.25rem 0px;
|
||||
align-items: center;
|
||||
|
||||
|
@ -27,14 +27,19 @@
|
|||
.color-picker__top-picks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-picker__button {
|
||||
--radius: 6px;
|
||||
--radius: 4px;
|
||||
--size: 1.375rem;
|
||||
|
||||
&.has-outline {
|
||||
box-shadow: inset 0 0 0 1px #d9d9d9;
|
||||
}
|
||||
|
||||
padding: 0;
|
||||
margin: 1px;
|
||||
margin: 0;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border: 0;
|
||||
|
@ -46,15 +51,19 @@
|
|||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover:not(.active) {
|
||||
&:hover:not(.active):not(.color-picker__button--large) {
|
||||
transform: scale(1.075);
|
||||
}
|
||||
|
||||
&:hover:not(.active).color-picker__button--large {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-shadow: 0 0 0 1px var(--swatch-color);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
box-shadow: 0 0 0 1px var(--color-gray-30);
|
||||
border-radius: var(--radius);
|
||||
filter: var(--theme-filter);
|
||||
}
|
||||
|
@ -70,7 +79,7 @@
|
|||
bottom: var(--offset);
|
||||
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
||||
z-index: 1; // due hover state so this has preference
|
||||
border-radius: calc(var(--radius) + 1px);
|
||||
border-radius: var(--radius);
|
||||
filter: var(--theme-filter);
|
||||
}
|
||||
}
|
||||
|
@ -125,10 +134,11 @@
|
|||
|
||||
.color-picker__button__hotkey-label {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
right: 5px;
|
||||
bottom: 3px;
|
||||
filter: none;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
|
|
|
@ -2,7 +2,11 @@ import * as Popover from "@radix-ui/react-popover";
|
|||
import clsx from "clsx";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { COLOR_PALETTE, isTransparent } from "@excalidraw/common";
|
||||
import {
|
||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||
COLOR_PALETTE,
|
||||
isTransparent,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common";
|
||||
|
||||
|
@ -19,7 +23,7 @@ import { ColorInput } from "./ColorInput";
|
|||
import { Picker } from "./Picker";
|
||||
import PickerHeading from "./PickerHeading";
|
||||
import { TopPicks } from "./TopPicks";
|
||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||
import { activeColorPickerSectionAtom, isColorDark } from "./colorPickerUtils";
|
||||
|
||||
import "./ColorPicker.scss";
|
||||
|
||||
|
@ -190,6 +194,7 @@ const ColorPickerTrigger = ({
|
|||
type="button"
|
||||
className={clsx("color-picker__button active-color properties-trigger", {
|
||||
"is-transparent": color === "transparent" || !color,
|
||||
"has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
|
||||
})}
|
||||
aria-label={label}
|
||||
style={color ? { "--swatch-color": color } : undefined}
|
||||
|
|
|
@ -40,7 +40,7 @@ export const CustomColorList = ({
|
|||
tabIndex={-1}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"color-picker__button color-picker__button--large",
|
||||
"color-picker__button color-picker__button--large has-outline",
|
||||
{
|
||||
active: color === c,
|
||||
"is-transparent": c === "transparent" || !c,
|
||||
|
@ -56,7 +56,7 @@ export const CustomColorList = ({
|
|||
key={i}
|
||||
>
|
||||
<div className="color-picker__button-outline" />
|
||||
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
|
||||
<HotkeyLabel color={c} keyLabel={i + 1} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import React from "react";
|
||||
|
||||
import { getContrastYIQ } from "./colorPickerUtils";
|
||||
import { isColorDark } from "./colorPickerUtils";
|
||||
|
||||
interface HotkeyLabelProps {
|
||||
color: string;
|
||||
keyLabel: string | number;
|
||||
isCustomColor?: boolean;
|
||||
isShade?: boolean;
|
||||
}
|
||||
const HotkeyLabel = ({
|
||||
color,
|
||||
keyLabel,
|
||||
isCustomColor = false,
|
||||
isShade = false,
|
||||
}: HotkeyLabelProps) => {
|
||||
return (
|
||||
<div
|
||||
className="color-picker__button__hotkey-label"
|
||||
style={{
|
||||
color: getContrastYIQ(color, isCustomColor),
|
||||
color: isColorDark(color) ? "#fff" : "#000",
|
||||
}}
|
||||
>
|
||||
{isShade && "⇧"}
|
||||
|
|
|
@ -65,7 +65,7 @@ const PickerColorList = ({
|
|||
tabIndex={-1}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"color-picker__button color-picker__button--large",
|
||||
"color-picker__button color-picker__button--large has-outline",
|
||||
{
|
||||
active: colorObj?.colorName === key,
|
||||
"is-transparent": color === "transparent" || !color,
|
||||
|
|
|
@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
|||
key={i}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"color-picker__button color-picker__button--large",
|
||||
"color-picker__button color-picker__button--large has-outline",
|
||||
{ active: i === shade },
|
||||
)}
|
||||
aria-label="Shade"
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||
DEFAULT_CANVAS_BACKGROUND_PICKS,
|
||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||
DEFAULT_ELEMENT_STROKE_PICKS,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { isColorDark } from "./colorPickerUtils";
|
||||
|
||||
import type { ColorPickerType } from "./colorPickerUtils";
|
||||
|
||||
interface TopPicksProps {
|
||||
|
@ -51,6 +54,10 @@ export const TopPicks = ({
|
|||
className={clsx("color-picker__button", {
|
||||
active: color === activeColor,
|
||||
"is-transparent": color === "transparent" || !color,
|
||||
"has-outline": !isColorDark(
|
||||
color,
|
||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||
),
|
||||
})}
|
||||
style={{ "--swatch-color": color }}
|
||||
key={color}
|
||||
|
|
|
@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType =
|
|||
export const activeColorPickerSectionAtom =
|
||||
atom<ActiveColorPickerSectionAtomType>(null);
|
||||
|
||||
const calculateContrast = (r: number, g: number, b: number) => {
|
||||
const calculateContrast = (r: number, g: number, b: number): number => {
|
||||
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return yiq >= 160 ? "black" : "white";
|
||||
return yiq;
|
||||
};
|
||||
|
||||
// inspiration from https://stackoverflow.com/a/11868398
|
||||
export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
|
||||
if (isCustomColor) {
|
||||
const style = new Option().style;
|
||||
style.color = bgHex;
|
||||
// YIQ algo, inspiration from https://stackoverflow.com/a/11868398
|
||||
export const isColorDark = (color: string, threshold = 160): boolean => {
|
||||
// no color ("") -> assume it default to black
|
||||
if (!color) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (style.color) {
|
||||
const rgb = style.color
|
||||
if (color === "transparent") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// a string color (white etc) or any other format -> convert to rgb by way
|
||||
// of creating a DOM node and retrieving the computeStyle
|
||||
if (!color.startsWith("#")) {
|
||||
const node = document.createElement("div");
|
||||
node.style.color = color;
|
||||
|
||||
if (node.style.color) {
|
||||
// making invisible so document doesn't reflow (hopefully).
|
||||
// display=none works too, but supposedly not in all browsers
|
||||
node.style.position = "absolute";
|
||||
node.style.visibility = "hidden";
|
||||
node.style.width = "0";
|
||||
node.style.height = "0";
|
||||
|
||||
// needs to be in DOM else browser won't compute the style
|
||||
document.body.appendChild(node);
|
||||
const computedColor = getComputedStyle(node).color;
|
||||
document.body.removeChild(node);
|
||||
// computed style is in rgb() format
|
||||
const rgb = computedColor
|
||||
.replace(/^(rgb|rgba)\(/, "")
|
||||
.replace(/\)$/, "")
|
||||
.replace(/\s/g, "")
|
||||
|
@ -114,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
|
|||
const g = parseInt(rgb[1]);
|
||||
const b = parseInt(rgb[2]);
|
||||
|
||||
return calculateContrast(r, g, b);
|
||||
return calculateContrast(r, g, b) < threshold;
|
||||
}
|
||||
// invalid color -> assume it default to black
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: ? is this wanted?
|
||||
if (bgHex === "transparent") {
|
||||
return "black";
|
||||
}
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
const r = parseInt(bgHex.substring(1, 3), 16);
|
||||
const g = parseInt(bgHex.substring(3, 5), 16);
|
||||
const b = parseInt(bgHex.substring(5, 7), 16);
|
||||
|
||||
return calculateContrast(r, g, b);
|
||||
return calculateContrast(r, g, b) < threshold;
|
||||
};
|
||||
|
||||
export type ColorPickerType =
|
||||
|
|
|
@ -317,6 +317,7 @@ function CommandPaletteInner({
|
|||
const toolCommands: CommandPaletteItem[] = [
|
||||
actionManager.actions.toggleHandTool,
|
||||
actionManager.actions.setFrameAsActiveTool,
|
||||
actionManager.actions.toggleLassoTool,
|
||||
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
|
||||
|
||||
const editorCommands: CommandPaletteItem[] = [
|
||||
|
|
|
@ -120,7 +120,7 @@ const getHints = ({
|
|||
!appState.editingTextElement &&
|
||||
!appState.editingLinearElement
|
||||
) {
|
||||
return t("hints.deepBoxSelect");
|
||||
return [t("hints.deepBoxSelect")];
|
||||
}
|
||||
|
||||
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
|
||||
|
@ -128,7 +128,7 @@ const getHints = ({
|
|||
}
|
||||
|
||||
if (!selectedElements.length && !isMobile) {
|
||||
return t("hints.canvasPanning");
|
||||
return [t("hints.canvasPanning")];
|
||||
}
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
|||
type: "everything",
|
||||
elements: item.elements,
|
||||
randomizeSeed: true,
|
||||
}).newElements,
|
||||
}).duplicatedElements,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
|
|||
selectionNonce: number | undefined;
|
||||
scale: number;
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderScrollbars: boolean;
|
||||
device: Device;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
|
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||
remotePointerUsernames,
|
||||
remotePointerUserStates,
|
||||
selectionColor,
|
||||
renderScrollbars: false,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
},
|
||||
device: props.device,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
|
@ -230,7 +231,8 @@ const areEqual = (
|
|||
// on appState)
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||
prevProps.selectedElements !== nextProps.selectedElements
|
||||
prevProps.selectedElements !== nextProps.selectedElements ||
|
||||
prevProps.renderScrollbars !== nextProps.renderScrollbars
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -87,34 +87,36 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
|||
return <div className="excalidraw__canvas-wrapper" ref={wrapperRef} />;
|
||||
};
|
||||
|
||||
const getRelevantAppStateProps = (
|
||||
appState: AppState,
|
||||
): StaticCanvasAppState => ({
|
||||
zoom: appState.zoom,
|
||||
scrollX: appState.scrollX,
|
||||
scrollY: appState.scrollY,
|
||||
width: appState.width,
|
||||
height: appState.height,
|
||||
viewModeEnabled: appState.viewModeEnabled,
|
||||
openDialog: appState.openDialog,
|
||||
hoveredElementIds: appState.hoveredElementIds,
|
||||
offsetLeft: appState.offsetLeft,
|
||||
offsetTop: appState.offsetTop,
|
||||
theme: appState.theme,
|
||||
pendingImageElementId: appState.pendingImageElementId,
|
||||
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
exportScale: appState.exportScale,
|
||||
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
|
||||
gridSize: appState.gridSize,
|
||||
gridStep: appState.gridStep,
|
||||
frameRendering: appState.frameRendering,
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
frameToHighlight: appState.frameToHighlight,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||
croppingElementId: appState.croppingElementId,
|
||||
});
|
||||
const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
|
||||
const relevantAppStateProps = {
|
||||
zoom: appState.zoom,
|
||||
scrollX: appState.scrollX,
|
||||
scrollY: appState.scrollY,
|
||||
width: appState.width,
|
||||
height: appState.height,
|
||||
viewModeEnabled: appState.viewModeEnabled,
|
||||
openDialog: appState.openDialog,
|
||||
hoveredElementIds: appState.hoveredElementIds,
|
||||
offsetLeft: appState.offsetLeft,
|
||||
offsetTop: appState.offsetTop,
|
||||
theme: appState.theme,
|
||||
pendingImageElementId: appState.pendingImageElementId,
|
||||
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
exportScale: appState.exportScale,
|
||||
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
|
||||
gridSize: appState.gridSize,
|
||||
gridStep: appState.gridStep,
|
||||
frameRendering: appState.frameRendering,
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
frameToHighlight: appState.frameToHighlight,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||
croppingElementId: appState.croppingElementId,
|
||||
};
|
||||
|
||||
return relevantAppStateProps;
|
||||
};
|
||||
|
||||
const areEqual = (
|
||||
prevProps: StaticCanvasProps,
|
||||
|
|
|
@ -274,6 +274,21 @@ export const SelectionIcon = createIcon(
|
|||
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
|
||||
);
|
||||
|
||||
export const LassoIcon = createIcon(
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path d="M4.028 13.252c-.657 -.972 -1.028 -2.078 -1.028 -3.252c0 -3.866 4.03 -7 9 -7s9 3.134 9 7s-4.03 7 -9 7c-1.913 0 -3.686 -.464 -5.144 -1.255" />
|
||||
<path d="M5 15m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M5 17c0 1.42 .316 2.805 1 4" />
|
||||
</g>,
|
||||
|
||||
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
|
||||
);
|
||||
|
||||
// tabler-icons: square
|
||||
export const RectangleIcon = createIcon(
|
||||
<g strokeWidth="1.5">
|
||||
|
@ -406,7 +421,7 @@ export const TrashIcon = createIcon(
|
|||
);
|
||||
|
||||
export const EmbedIcon = createIcon(
|
||||
<g strokeWidth="1.25">
|
||||
<g strokeWidth="1.5">
|
||||
<polyline points="12 16 18 10 12 4" />
|
||||
<polyline points="8 4 2 10 8 16" />
|
||||
</g>,
|
||||
|
|
|
@ -173,7 +173,7 @@ body.excalidraw-cursor-resize * {
|
|||
.buttonList {
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
column-gap: 0.375rem;
|
||||
column-gap: 0.5rem;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
label {
|
||||
|
@ -386,16 +386,10 @@ body.excalidraw-cursor-resize * {
|
|||
|
||||
.App-menu__left {
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem 0.75rem 0.25rem 0.75rem;
|
||||
width: 11.875rem;
|
||||
padding: 0.75rem;
|
||||
width: 12.5rem;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
|
||||
.buttonList label,
|
||||
.buttonList button,
|
||||
.buttonList .zIndexButton {
|
||||
--button-bg: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-select {
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
--border-radius-lg: 0.5rem;
|
||||
|
||||
--color-surface-high: #f1f0ff;
|
||||
--color-surface-mid: #f2f2f7;
|
||||
--color-surface-mid: #f6f6f9;
|
||||
--color-surface-low: #ececf4;
|
||||
--color-surface-lowest: #ffffff;
|
||||
--color-on-surface: #1b1b1f;
|
||||
|
@ -252,7 +252,7 @@
|
|||
|
||||
--color-logo-text: #e2dfff;
|
||||
|
||||
--color-surface-high: hsl(245, 10%, 21%);
|
||||
--color-surface-high: #2e2d39;
|
||||
--color-surface-low: hsl(240, 8%, 15%);
|
||||
--color-surface-mid: hsl(240 6% 10%);
|
||||
--color-surface-lowest: hsl(0, 0%, 7%);
|
||||
|
|
|
@ -104,12 +104,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
394.5,
|
||||
34.5,
|
||||
394,
|
||||
34,
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -129,8 +129,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 395,
|
||||
"x": 247,
|
||||
"y": 420,
|
||||
"x": 247.5,
|
||||
"y": 420.5,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -160,11 +160,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
399.5,
|
||||
399,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -185,7 +185,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 400,
|
||||
"x": 227,
|
||||
"x": 227.5,
|
||||
"y": 450,
|
||||
}
|
||||
`;
|
||||
|
@ -350,11 +350,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -375,7 +375,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 255,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
`;
|
||||
|
@ -452,11 +452,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -477,7 +477,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 255,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
`;
|
||||
|
@ -628,11 +628,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -653,7 +653,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 255,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
`;
|
||||
|
@ -845,11 +845,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"x": 100.5,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
@ -893,11 +893,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -914,7 +914,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 450,
|
||||
"x": 450.5,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
@ -1490,11 +1490,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
272.485,
|
||||
271.985,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -1517,7 +1517,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 272.985,
|
||||
"x": 111.262,
|
||||
"x": 111.762,
|
||||
"y": 57,
|
||||
}
|
||||
`;
|
||||
|
@ -1862,11 +1862,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -1883,7 +1883,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"x": 100.5,
|
||||
"y": 100,
|
||||
}
|
||||
`;
|
||||
|
@ -1915,11 +1915,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -1936,7 +1936,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"x": 100.5,
|
||||
"y": 200,
|
||||
}
|
||||
`;
|
||||
|
@ -1968,11 +1968,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -1989,7 +1989,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"x": 100.5,
|
||||
"y": 300,
|
||||
}
|
||||
`;
|
||||
|
@ -2021,11 +2021,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
0.5,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
99.5,
|
||||
99,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -2042,7 +2042,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||
"version": 2,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"x": 100.5,
|
||||
"y": 400,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
isFirefox,
|
||||
MIME_TYPES,
|
||||
cloneJSON,
|
||||
SVG_DOCUMENT_PREAMBLE,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
@ -134,7 +135,11 @@ export const exportCanvas = async (
|
|||
if (type === "svg") {
|
||||
return fileSave(
|
||||
svgPromise.then((svg) => {
|
||||
return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
|
||||
// adding SVG preamble so that older software parse the SVG file
|
||||
// properly
|
||||
return new Blob([SVG_DOCUMENT_PREAMBLE + svg.outerHTML], {
|
||||
type: MIME_TYPES.svg,
|
||||
});
|
||||
}),
|
||||
{
|
||||
description: "Export to SVG",
|
||||
|
|
|
@ -86,6 +86,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||
boolean
|
||||
> = {
|
||||
selection: true,
|
||||
lasso: true,
|
||||
text: true,
|
||||
rectangle: true,
|
||||
diamond: true,
|
||||
|
@ -438,7 +439,7 @@ const repairContainerElement = (
|
|||
// if defined, lest boundElements is stale
|
||||
!boundElement.containerId
|
||||
) {
|
||||
(boundElement as Mutable<ExcalidrawTextElement>).containerId =
|
||||
(boundElement as Mutable<typeof boundElement>).containerId =
|
||||
container.id;
|
||||
}
|
||||
}
|
||||
|
@ -463,6 +464,10 @@ const repairBoundElement = (
|
|||
? elementsMap.get(boundElement.containerId)
|
||||
: null;
|
||||
|
||||
(boundElement as Mutable<typeof boundElement>).angle = (
|
||||
isArrowElement(container) ? 0 : container?.angle ?? 0
|
||||
) as Radians;
|
||||
|
||||
if (!container) {
|
||||
boundElement.containerId = null;
|
||||
return;
|
||||
|
|
|
@ -427,7 +427,7 @@ describe("Test Transform", () => {
|
|||
const [arrow, text, rectangle, ellipse] = excalidrawElements;
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255,
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
startBinding: {
|
||||
|
@ -512,7 +512,7 @@ describe("Test Transform", () => {
|
|||
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255,
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [{ id: text1.id, type: "text" }],
|
||||
startBinding: {
|
||||
|
@ -730,7 +730,7 @@ describe("Test Transform", () => {
|
|||
const [, , arrow, text] = excalidrawElements;
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255,
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [
|
||||
{
|
||||
|
|
|
@ -36,6 +36,8 @@ import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
|
|||
|
||||
import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
||||
|
||||
import type {
|
||||
|
@ -463,7 +465,13 @@ const bindLinearElementToElement = (
|
|||
newPoints[endPointIndex][1] += delta;
|
||||
}
|
||||
|
||||
Object.assign(linearElement, { points: newPoints });
|
||||
Object.assign(
|
||||
linearElement,
|
||||
LinearElementEditor.getNormalizedPoints({
|
||||
...linearElement,
|
||||
points: newPoints,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
linearElement,
|
||||
|
|
243
packages/excalidraw/eraser/index.ts
Normal file
243
packages/excalidraw/eraser/index.ts
Normal file
|
@ -0,0 +1,243 @@
|
|||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||
import { getElementLineSegments } from "@excalidraw/element/bounds";
|
||||
import {
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||
|
||||
import { getElementShape } from "@excalidraw/element/shapes";
|
||||
import { shouldTestInside } from "@excalidraw/element/collision";
|
||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBoundToContainer,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
import { getBoundTextElementId } from "@excalidraw/element/textElement";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { AnimatedTrail } from "../animated-trail";
|
||||
|
||||
import type { AnimationFrameHandler } from "../animation-frame-handler";
|
||||
|
||||
import type App from "../components/App";
|
||||
|
||||
// just enough to form a segment; this is sufficient for eraser
|
||||
const POINTS_ON_TRAIL = 2;
|
||||
|
||||
export class EraserTrail extends AnimatedTrail {
|
||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||
new Map();
|
||||
|
||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||
super(animationFrameHandler, app, {
|
||||
streamline: 0.2,
|
||||
size: 5,
|
||||
keepHead: true,
|
||||
sizeMapping: (c) => {
|
||||
const DECAY_TIME = 200;
|
||||
const DECAY_LENGTH = 10;
|
||||
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: () =>
|
||||
app.state.theme === THEME.LIGHT
|
||||
? "rgba(0, 0, 0, 0.2)"
|
||||
: "rgba(255, 255, 255, 0.2)",
|
||||
});
|
||||
}
|
||||
|
||||
startPath(x: number, y: number): void {
|
||||
this.endPath();
|
||||
super.startPath(x, y);
|
||||
this.elementsToErase.clear();
|
||||
}
|
||||
|
||||
addPointToPath(x: number, y: number, restore = false) {
|
||||
super.addPointToPath(x, y);
|
||||
|
||||
const elementsToEraser = this.updateElementsToBeErased(restore);
|
||||
|
||||
return elementsToEraser;
|
||||
}
|
||||
|
||||
private updateElementsToBeErased(restoreToErase?: boolean) {
|
||||
let eraserPath: GlobalPoint[] =
|
||||
super
|
||||
.getCurrentTrail()
|
||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
||||
|
||||
// for efficiency and avoid unnecessary calculations,
|
||||
// take only POINTS_ON_TRAIL points to form some number of segments
|
||||
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
|
||||
|
||||
const candidateElements = this.app.visibleElements.filter(
|
||||
(el) => !el.locked,
|
||||
);
|
||||
|
||||
const candidateElementsMap = arrayToMap(candidateElements);
|
||||
|
||||
const pathSegments = eraserPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(eraserPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const element of candidateElements) {
|
||||
// restore only if already added to the to-be-erased set
|
||||
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
|
||||
if (intersects) {
|
||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||
|
||||
if (this.groupsToErase.has(shallowestGroupId)) {
|
||||
const elementsInGroup = getElementsInGroup(
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
shallowestGroupId,
|
||||
);
|
||||
for (const elementInGroup of elementsInGroup) {
|
||||
this.elementsToErase.delete(elementInGroup.id);
|
||||
}
|
||||
this.groupsToErase.delete(shallowestGroupId);
|
||||
}
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
this.elementsToErase.delete(element.containerId);
|
||||
}
|
||||
|
||||
if (hasBoundTextElement(element)) {
|
||||
const boundText = getBoundTextElementId(element);
|
||||
|
||||
if (boundText) {
|
||||
this.elementsToErase.delete(boundText);
|
||||
}
|
||||
}
|
||||
|
||||
this.elementsToErase.delete(element.id);
|
||||
}
|
||||
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
|
||||
if (intersects) {
|
||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||
|
||||
if (!this.groupsToErase.has(shallowestGroupId)) {
|
||||
const elementsInGroup = getElementsInGroup(
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
shallowestGroupId,
|
||||
);
|
||||
|
||||
for (const elementInGroup of elementsInGroup) {
|
||||
this.elementsToErase.add(elementInGroup.id);
|
||||
}
|
||||
this.groupsToErase.add(shallowestGroupId);
|
||||
}
|
||||
|
||||
if (hasBoundTextElement(element)) {
|
||||
const boundText = getBoundTextElementId(element);
|
||||
|
||||
if (boundText) {
|
||||
this.elementsToErase.add(boundText);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
this.elementsToErase.add(element.containerId);
|
||||
}
|
||||
|
||||
this.elementsToErase.add(element.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(this.elementsToErase);
|
||||
}
|
||||
|
||||
endPath(): void {
|
||||
super.endPath();
|
||||
super.clearTrails();
|
||||
this.elementsToErase.clear();
|
||||
this.groupsToErase.clear();
|
||||
this.segmentsCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const eraserTest = (
|
||||
pathSegments: LineSegment<GlobalPoint>[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||
elementsMap: ElementsMap,
|
||||
app: App,
|
||||
): boolean => {
|
||||
let shape = shapesCache.get(element.id);
|
||||
|
||||
if (!shape) {
|
||||
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
||||
shapesCache.set(element.id, shape);
|
||||
}
|
||||
|
||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let elementSegments = elementsSegments.get(element.id);
|
||||
|
||||
if (!elementSegments) {
|
||||
elementSegments = getElementLineSegments(element, elementsMap);
|
||||
elementsSegments.set(element.id, elementSegments);
|
||||
}
|
||||
|
||||
return pathSegments.some((pathSegment) =>
|
||||
elementSegments?.some(
|
||||
(elementSegment) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
pathSegment,
|
||||
elementSegment,
|
||||
app.getElementHitThreshold(),
|
||||
) !== null,
|
||||
),
|
||||
);
|
||||
};
|
|
@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
renderEmbeddable,
|
||||
aiEnabled,
|
||||
showDeprecatedFonts,
|
||||
renderScrollbars,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
|
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
renderEmbeddable={renderEmbeddable}
|
||||
aiEnabled={aiEnabled !== false}
|
||||
showDeprecatedFonts={showDeprecatedFonts}
|
||||
renderScrollbars={renderScrollbars}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
|
|
201
packages/excalidraw/lasso/index.ts
Normal file
201
packages/excalidraw/lasso/index.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import {
|
||||
type GlobalPoint,
|
||||
type LineSegment,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getElementLineSegments } from "@excalidraw/element/bounds";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getFrameChildren } from "@excalidraw/element/frame";
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
|
||||
import { arrayToMap, easeOut } from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
NonDeleted,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { type AnimationFrameHandler } from "../animation-frame-handler";
|
||||
|
||||
import { AnimatedTrail } from "../animated-trail";
|
||||
|
||||
import { getLassoSelectedElementIds } from "./utils";
|
||||
|
||||
import type App from "../components/App";
|
||||
|
||||
export class LassoTrail extends AnimatedTrail {
|
||||
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null =
|
||||
null;
|
||||
private keepPreviousSelection: boolean = false;
|
||||
|
||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||
super(animationFrameHandler, app, {
|
||||
animateTrail: true,
|
||||
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: () => "rgba(105,101,219,0.05)",
|
||||
stroke: () => "rgba(105,101,219)",
|
||||
});
|
||||
}
|
||||
|
||||
startPath(x: number, y: number, keepPreviousSelection = false) {
|
||||
// clear any existing trails just in case
|
||||
this.endPath();
|
||||
|
||||
super.startPath(x, y);
|
||||
this.intersectedElements.clear();
|
||||
this.enclosedElements.clear();
|
||||
|
||||
this.keepPreviousSelection = keepPreviousSelection;
|
||||
|
||||
if (!this.keepPreviousSelection) {
|
||||
this.app.setState({
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
selectedLinearElement: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectElementsFromIds = (ids: string[]) => {
|
||||
this.app.setState((prevState) => {
|
||||
const nextSelectedElementIds = ids.reduce((acc, id) => {
|
||||
acc[id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>);
|
||||
|
||||
if (this.keepPreviousSelection) {
|
||||
for (const id of Object.keys(prevState.selectedElementIds)) {
|
||||
nextSelectedElementIds[id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id] of Object.entries(nextSelectedElementIds)) {
|
||||
const element = this.app.scene.getNonDeletedElement(id);
|
||||
|
||||
if (element && isTextElement(element)) {
|
||||
const container = getContainerElement(
|
||||
element,
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (container) {
|
||||
nextSelectedElementIds[container.id] = true;
|
||||
delete nextSelectedElementIds[element.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all children of selected frames
|
||||
for (const [id] of Object.entries(nextSelectedElementIds)) {
|
||||
const element = this.app.scene.getNonDeletedElement(id);
|
||||
|
||||
if (element && isFrameLikeElement(element)) {
|
||||
const elementsInFrame = getFrameChildren(
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
element.id,
|
||||
);
|
||||
for (const child of elementsInFrame) {
|
||||
delete nextSelectedElementIds[child.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextSelection = selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
},
|
||||
this.app.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this.app,
|
||||
);
|
||||
|
||||
const selectedIds = [...Object.keys(nextSelection.selectedElementIds)];
|
||||
const selectedGroupIds = [...Object.keys(nextSelection.selectedGroupIds)];
|
||||
|
||||
return {
|
||||
selectedElementIds: nextSelection.selectedElementIds,
|
||||
selectedGroupIds: nextSelection.selectedGroupIds,
|
||||
selectedLinearElement:
|
||||
selectedIds.length === 1 &&
|
||||
!selectedGroupIds.length &&
|
||||
isLinearElement(this.app.scene.getNonDeletedElement(selectedIds[0]))
|
||||
? new LinearElementEditor(
|
||||
this.app.scene.getNonDeletedElement(
|
||||
selectedIds[0],
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
addPointToPath = (x: number, y: number, keepPreviousSelection = false) => {
|
||||
super.addPointToPath(x, y);
|
||||
|
||||
this.keepPreviousSelection = keepPreviousSelection;
|
||||
|
||||
this.updateSelection();
|
||||
};
|
||||
|
||||
private updateSelection = () => {
|
||||
const lassoPath = super
|
||||
.getCurrentTrail()
|
||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
|
||||
|
||||
if (!this.elementsSegments) {
|
||||
this.elementsSegments = new Map();
|
||||
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
||||
for (const element of this.app.visibleElements) {
|
||||
const segments = getElementLineSegments(element, visibleElementsMap);
|
||||
this.elementsSegments.set(element.id, segments);
|
||||
}
|
||||
}
|
||||
|
||||
if (lassoPath) {
|
||||
const { selectedElementIds } = getLassoSelectedElementIds({
|
||||
lassoPath,
|
||||
elements: this.app.visibleElements,
|
||||
elementsSegments: this.elementsSegments,
|
||||
intersectedElements: this.intersectedElements,
|
||||
enclosedElements: this.enclosedElements,
|
||||
simplifyDistance: 5 / this.app.state.zoom.value,
|
||||
});
|
||||
|
||||
this.selectElementsFromIds(selectedElementIds);
|
||||
}
|
||||
};
|
||||
|
||||
endPath(): void {
|
||||
super.endPath();
|
||||
super.clearTrails();
|
||||
this.intersectedElements.clear();
|
||||
this.enclosedElements.clear();
|
||||
this.elementsSegments = null;
|
||||
}
|
||||
}
|
109
packages/excalidraw/lasso/utils.ts
Normal file
109
packages/excalidraw/lasso/utils.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { simplify } from "points-on-curve";
|
||||
|
||||
import {
|
||||
polygonFromPoints,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
export const getLassoSelectedElementIds = (input: {
|
||||
lassoPath: GlobalPoint[];
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elementsSegments: ElementsSegmentsMap;
|
||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||
simplifyDistance?: number;
|
||||
}): {
|
||||
selectedElementIds: string[];
|
||||
} => {
|
||||
const {
|
||||
lassoPath,
|
||||
elements,
|
||||
elementsSegments,
|
||||
intersectedElements,
|
||||
enclosedElements,
|
||||
simplifyDistance,
|
||||
} = input;
|
||||
// simplify the path to reduce the number of points
|
||||
let path: GlobalPoint[] = lassoPath;
|
||||
if (simplifyDistance) {
|
||||
path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
|
||||
}
|
||||
// as the path might not enclose a shape anymore, clear before checking
|
||||
enclosedElements.clear();
|
||||
for (const element of elements) {
|
||||
if (
|
||||
!intersectedElements.has(element.id) &&
|
||||
!enclosedElements.has(element.id)
|
||||
) {
|
||||
const enclosed = enclosureTest(path, element, elementsSegments);
|
||||
if (enclosed) {
|
||||
enclosedElements.add(element.id);
|
||||
} else {
|
||||
const intersects = intersectionTest(path, element, elementsSegments);
|
||||
if (intersects) {
|
||||
intersectedElements.add(element.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results = [...intersectedElements, ...enclosedElements];
|
||||
|
||||
return {
|
||||
selectedElementIds: results,
|
||||
};
|
||||
};
|
||||
|
||||
const enclosureTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
): boolean => {
|
||||
const lassoPolygon = polygonFromPoints(lassoPath);
|
||||
const segments = elementsSegments.get(element.id);
|
||||
if (!segments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return segments.some((segment) => {
|
||||
return segment.some((point) =>
|
||||
polygonIncludesPointNonZero(point, lassoPolygon),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const intersectionTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
): boolean => {
|
||||
const elementSegments = elementsSegments.get(element.id);
|
||||
if (!elementSegments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
|
||||
return lassoSegments.some((lassoSegment) =>
|
||||
elementSegments.some(
|
||||
(elementSegment) =>
|
||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||
),
|
||||
);
|
||||
};
|
|
@ -278,6 +278,7 @@
|
|||
},
|
||||
"toolBar": {
|
||||
"selection": "Selection",
|
||||
"lasso": "Lasso selection",
|
||||
"image": "Insert image",
|
||||
"rectangle": "Rectangle",
|
||||
"diamond": "Diamond",
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
"@radix-ui/react-popover": "1.1.6",
|
||||
"@radix-ui/react-tabs": "1.0.2",
|
||||
"@radix-ui/react-tabs": "1.1.3",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"canvas-roundrect-polyfill": "0.0.1",
|
||||
"clsx": "1.1.1",
|
||||
|
|
|
@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
|
|||
let scrollBars;
|
||||
if (renderConfig.renderScrollbars) {
|
||||
scrollBars = getScrollBars(
|
||||
visibleElements,
|
||||
elementsMap,
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
appState,
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
toBrandedType,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
isReadonlyArray,
|
||||
} from "@excalidraw/common";
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||
|
@ -292,11 +293,9 @@ class Scene {
|
|||
}
|
||||
|
||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||
const _nextElements =
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
nextElements instanceof Array
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
const _nextElements = isReadonlyArray(nextElements)
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
|
||||
validateIndicesThrottled(_nextElements);
|
||||
|
|
|
@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
|
|||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { getLanguage } from "../i18n";
|
||||
|
||||
import type { InteractiveCanvasAppState } from "../types";
|
||||
import type { ScrollBars } from "./types";
|
||||
import type { RenderableElementsMap, ScrollBars } from "./types";
|
||||
|
||||
export const SCROLLBAR_MARGIN = 4;
|
||||
export const SCROLLBAR_WIDTH = 6;
|
||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||
|
||||
// The scrollbar represents where the viewport is in relationship to the scene
|
||||
export const getScrollBars = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: RenderableElementsMap,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (!elements.length) {
|
||||
if (!elements.size) {
|
||||
return {
|
||||
horizontal: null,
|
||||
vertical: null,
|
||||
|
@ -33,9 +32,6 @@ export const getScrollBars = (
|
|||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||
|
||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||
|
||||
const safeArea = {
|
||||
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
||||
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
||||
|
@ -46,10 +42,8 @@ export const getScrollBars = (
|
|||
const isRTL = getLanguage().rtl;
|
||||
|
||||
// The viewport is the rectangle currently visible for the user
|
||||
const viewportMinX =
|
||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY =
|
||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMinX = -appState.scrollX + safeArea.left;
|
||||
const viewportMinY = -appState.scrollY + safeArea.top;
|
||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||
|
||||
|
@ -59,8 +53,43 @@ export const getScrollBars = (
|
|||
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
||||
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
||||
|
||||
// The scrollbar represents where the viewport is in relationship to the scene
|
||||
// the elements-only bbox
|
||||
const sceneWidth = elementsMaxX - elementsMinX;
|
||||
const sceneHeight = elementsMaxY - elementsMinY;
|
||||
|
||||
// scene (elements) bbox + the viewport bbox that extends outside of it
|
||||
const extendedSceneWidth = sceneMaxX - sceneMinX;
|
||||
const extendedSceneHeight = sceneMaxY - sceneMinY;
|
||||
|
||||
const scrollWidthOffset =
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) +
|
||||
SCROLLBAR_WIDTH * 2;
|
||||
|
||||
const scrollbarWidth =
|
||||
viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) -
|
||||
scrollWidthOffset;
|
||||
|
||||
const scrollbarHeightOffset =
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) +
|
||||
SCROLLBAR_WIDTH * 2;
|
||||
|
||||
const scrollbarHeight =
|
||||
viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) -
|
||||
scrollbarHeightOffset;
|
||||
// NOTE the delta multiplier calculation isn't quite correct when viewport
|
||||
// is extended outside the scene (elements) bbox as there's some small
|
||||
// accumulation error. I'll let this be an exercise for others to fix. ^^
|
||||
const horizontalDeltaMultiplier =
|
||||
extendedSceneWidth > sceneWidth
|
||||
? (extendedSceneWidth * appState.zoom.value) /
|
||||
(scrollbarWidth + scrollWidthOffset)
|
||||
: viewportWidth / (scrollbarWidth + scrollWidthOffset);
|
||||
|
||||
const verticalDeltaMultiplier =
|
||||
extendedSceneHeight > sceneHeight
|
||||
? (extendedSceneHeight * appState.zoom.value) /
|
||||
(scrollbarHeight + scrollbarHeightOffset)
|
||||
: viewportHeight / (scrollbarHeight + scrollbarHeightOffset);
|
||||
return {
|
||||
horizontal:
|
||||
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
||||
|
@ -68,18 +97,17 @@ export const getScrollBars = (
|
|||
: {
|
||||
x:
|
||||
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
||||
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
|
||||
viewportWidth,
|
||||
SCROLLBAR_WIDTH +
|
||||
((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
|
||||
y:
|
||||
viewportHeight -
|
||||
SCROLLBAR_WIDTH -
|
||||
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
||||
width:
|
||||
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
|
||||
viewportWidth -
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
|
||||
width: scrollbarWidth,
|
||||
height: SCROLLBAR_WIDTH,
|
||||
deltaMultiplier: horizontalDeltaMultiplier,
|
||||
},
|
||||
|
||||
vertical:
|
||||
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
||||
? null
|
||||
|
@ -90,14 +118,13 @@ export const getScrollBars = (
|
|||
SCROLLBAR_WIDTH -
|
||||
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
||||
y:
|
||||
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
|
||||
viewportHeight +
|
||||
Math.max(safeArea.top, SCROLLBAR_MARGIN),
|
||||
Math.max(safeArea.top, SCROLLBAR_MARGIN) +
|
||||
SCROLLBAR_WIDTH +
|
||||
((viewportMinY - sceneMinY) / extendedSceneHeight) *
|
||||
viewportHeight,
|
||||
width: SCROLLBAR_WIDTH,
|
||||
height:
|
||||
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
|
||||
viewportHeight -
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
|
||||
height: scrollbarHeight,
|
||||
deltaMultiplier: verticalDeltaMultiplier,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -130,12 +130,14 @@ export type ScrollBars = {
|
|||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
deltaMultiplier: number;
|
||||
} | null;
|
||||
vertical: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
deltaMultiplier: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1088,6 +1089,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1307,6 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1641,6 +1644,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1975,6 +1979,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2194,6 +2199,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2437,6 +2443,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2741,6 +2748,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3113,6 +3121,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3591,6 +3600,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3917,6 +3927,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4243,6 +4254,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4649,6 +4661,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5870,6 +5883,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7137,6 +7151,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7408,7 +7423,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
</svg>,
|
||||
"label": "labels.elementLock.unlockAll",
|
||||
"name": "unlockAllElements",
|
||||
"paletteName": "Unlock all elements",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
|
@ -7559,7 +7573,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
"keyTest": [Function],
|
||||
"label": "buttons.zenMode",
|
||||
"name": "zenMode",
|
||||
"paletteName": "Toggle zen mode",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
|
@ -7603,7 +7616,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
"keyTest": [Function],
|
||||
"label": "labels.viewMode",
|
||||
"name": "viewMode",
|
||||
"paletteName": "Toggle view mode",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
|
@ -7677,7 +7689,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
],
|
||||
"label": "stats.fullTitle",
|
||||
"name": "stats",
|
||||
"paletteName": "Toggle stats",
|
||||
"perform": [Function],
|
||||
"trackEvent": {
|
||||
"category": "menu",
|
||||
|
@ -7814,6 +7825,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8802,6 +8814,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
|
|
@ -572,7 +572,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
class="color-picker__top-picks"
|
||||
>
|
||||
<button
|
||||
class="color-picker__button active"
|
||||
class="color-picker__button active has-outline"
|
||||
data-testid="color-top-pick-#ffffff"
|
||||
style="--swatch-color: #ffffff;"
|
||||
title="#ffffff"
|
||||
|
@ -583,7 +583,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
class="color-picker__button"
|
||||
class="color-picker__button has-outline"
|
||||
data-testid="color-top-pick-#f8f9fa"
|
||||
style="--swatch-color: #f8f9fa;"
|
||||
title="#f8f9fa"
|
||||
|
@ -594,7 +594,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
class="color-picker__button"
|
||||
class="color-picker__button has-outline"
|
||||
data-testid="color-top-pick-#f5faff"
|
||||
style="--swatch-color: #f5faff;"
|
||||
title="#f5faff"
|
||||
|
@ -605,7 +605,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
class="color-picker__button"
|
||||
class="color-picker__button has-outline"
|
||||
data-testid="color-top-pick-#fffce8"
|
||||
style="--swatch-color: #fffce8;"
|
||||
title="#fffce8"
|
||||
|
@ -616,7 +616,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
class="color-picker__button"
|
||||
class="color-picker__button has-outline"
|
||||
data-testid="color-top-pick-#fdf8f6"
|
||||
style="--swatch-color: #fdf8f6;"
|
||||
title="#fdf8f6"
|
||||
|
@ -635,7 +635,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-label="Canvas background"
|
||||
class="color-picker__button active-color properties-trigger"
|
||||
class="color-picker__button active-color properties-trigger has-outline"
|
||||
data-state="closed"
|
||||
style="--swatch-color: #ffffff;"
|
||||
title="Show background color picker"
|
||||
|
|
|
@ -5,6 +5,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -604,6 +605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1111,6 +1113,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1482,6 +1485,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1854,6 +1858,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2124,6 +2129,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2563,6 +2569,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2865,6 +2872,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3152,6 +3160,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3449,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3738,6 +3748,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3976,6 +3987,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4238,6 +4250,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4514,6 +4527,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4748,6 +4762,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4982,6 +4997,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5214,6 +5230,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5446,6 +5463,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5708,6 +5726,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -6042,6 +6061,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -6470,6 +6490,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -6851,6 +6872,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7173,6 +7195,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7325,8 +7348,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"updated": 1,
|
||||
"version": 7,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -7399,8 +7422,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -7474,6 +7497,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7706,6 +7730,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8064,6 +8089,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8422,6 +8448,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8829,6 +8856,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
|
@ -9119,6 +9147,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9387,6 +9416,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9654,6 +9684,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9888,6 +9919,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -10192,6 +10224,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -10535,6 +10568,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -10773,6 +10807,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11225,6 +11260,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11482,6 +11518,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11724,6 +11761,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11968,6 +12006,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
|
@ -12099,8 +12138,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12153,8 +12192,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 5,
|
||||
"width": 50,
|
||||
"x": 60,
|
||||
"y": 0,
|
||||
"x": 40,
|
||||
"y": -20,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12207,8 +12246,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"width": 50,
|
||||
"x": 150,
|
||||
"y": -10,
|
||||
"x": 130,
|
||||
"y": -30,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12262,8 +12301,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -12348,8 +12387,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "freedraw",
|
||||
"width": 50,
|
||||
"x": 150,
|
||||
"y": -10,
|
||||
"x": 130,
|
||||
"y": -30,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -12372,6 +12411,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -12622,6 +12662,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -12866,6 +12907,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13110,6 +13152,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13360,6 +13403,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13695,6 +13739,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13870,6 +13915,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14161,6 +14207,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14431,6 +14478,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14709,6 +14757,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14873,6 +14922,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -15570,6 +15620,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -16189,6 +16240,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -16808,6 +16860,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -17518,6 +17571,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -18265,6 +18319,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -18742,6 +18797,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -19267,6 +19323,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -19726,6 +19783,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
|
|
@ -1,40 +1,6 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 50,
|
||||
"id": "id2",
|
||||
"index": "Zz",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
|
@ -61,7 +27,41 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 50,
|
||||
"id": "id2",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1604849351,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 915032327,
|
||||
"width": 30,
|
||||
"x": -10,
|
||||
"y": 60,
|
||||
|
|
|
@ -5,6 +5,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -420,6 +421,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -826,6 +828,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1371,6 +1374,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1575,6 +1579,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -1950,6 +1955,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2032,7 +2038,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||
"scrolledOutside": false,
|
||||
"searchMatches": [],
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
|
@ -2122,8 +2128,16 @@ History {
|
|||
HistoryEntry {
|
||||
"appStateChange": AppStateChange {
|
||||
"delta": Delta {
|
||||
"deleted": {},
|
||||
"inserted": {},
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id2": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"elementsChange": ElementsChange {
|
||||
|
@ -2139,7 +2153,7 @@ History {
|
|||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"index": "Zz",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -2153,26 +2167,15 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {
|
||||
"id0" => Delta {
|
||||
"deleted": {
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2188,6 +2191,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2368,6 +2372,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2688,6 +2693,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -2934,6 +2940,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3177,6 +3184,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3407,6 +3415,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3663,6 +3672,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -3974,6 +3984,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4396,6 +4407,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4679,6 +4691,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -4932,6 +4945,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5142,6 +5156,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5341,6 +5356,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -5723,6 +5739,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -6013,6 +6030,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
|
@ -6821,6 +6839,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7151,6 +7170,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7427,6 +7447,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7661,6 +7682,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -7898,6 +7920,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8078,6 +8101,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8258,6 +8282,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8438,6 +8463,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8661,6 +8687,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -8883,6 +8910,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
|
@ -9077,6 +9105,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9300,6 +9329,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9480,6 +9510,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9702,6 +9733,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -9882,6 +9914,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
|
@ -10076,6 +10109,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -10256,6 +10290,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -10340,13 +10375,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||
"scrolledOutside": false,
|
||||
"searchMatches": [],
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
"id6": true,
|
||||
"id8": true,
|
||||
"id9": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {
|
||||
"id4": true,
|
||||
"id7": true,
|
||||
},
|
||||
"selectedLinearElement": null,
|
||||
"selectionElement": null,
|
||||
|
@ -10610,8 +10645,26 @@ History {
|
|||
HistoryEntry {
|
||||
"appStateChange": AppStateChange {
|
||||
"delta": Delta {
|
||||
"deleted": {},
|
||||
"inserted": {},
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id6": true,
|
||||
"id8": true,
|
||||
"id9": true,
|
||||
},
|
||||
"selectedGroupIds": {
|
||||
"id7": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedGroupIds": {
|
||||
"id4": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"elementsChange": ElementsChange {
|
||||
|
@ -10629,7 +10682,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zx",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10643,8 +10696,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -10662,7 +10715,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zy",
|
||||
"index": "a4",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10676,8 +10729,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
"x": 40,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -10695,7 +10748,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zz",
|
||||
"index": "a5",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10709,46 +10762,15 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
"x": 60,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {
|
||||
"id0" => Delta {
|
||||
"deleted": {
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
"id1" => Delta {
|
||||
"deleted": {
|
||||
"x": 40,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
"id2" => Delta {
|
||||
"deleted": {
|
||||
"x": 60,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -10764,6 +10786,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11041,6 +11064,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11167,6 +11191,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11366,6 +11391,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -11677,6 +11703,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -12089,6 +12116,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -12702,6 +12730,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -12831,6 +12860,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13415,6 +13445,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -13753,6 +13784,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14018,6 +14050,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14144,6 +14177,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
@ -14523,6 +14557,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "text",
|
||||
|
@ -14649,6 +14684,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
|
|
@ -307,6 +307,41 @@ describe("pasting & frames", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should remove element from frame when pasted outside", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame.id,
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
API.setElements([frame]);
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(150, 150);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(2);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out elements not overlapping frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
|
|
|
@ -218,7 +218,7 @@ describe("Cropping and other features", async () => {
|
|||
initialHeight / 2,
|
||||
]);
|
||||
Keyboard.keyDown(KEYS.ESCAPE);
|
||||
const duplicatedImage = duplicateElement(null, new Map(), image, {});
|
||||
const duplicatedImage = duplicateElement(null, new Map(), image);
|
||||
act(() => {
|
||||
h.app.scene.insertElement(duplicatedImage);
|
||||
});
|
||||
|
|
|
@ -444,7 +444,6 @@ export class API {
|
|||
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
id: "text2",
|
||||
width: 50,
|
||||
height: 20,
|
||||
containerId: arrow.id,
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
isTextElement,
|
||||
isFrameLikeElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
import { KEYS, arrayToMap } from "@excalidraw/common";
|
||||
import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
|
@ -151,7 +151,7 @@ export class Keyboard {
|
|||
const getElementPointForSelection = (
|
||||
element: ExcalidrawElement,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const { x, y, width, angle } = element;
|
||||
const target = pointFrom<GlobalPoint>(
|
||||
x +
|
||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||
|
@ -166,7 +166,7 @@ const getElementPointForSelection = (
|
|||
(bounds[1] + bounds[3]) / 2,
|
||||
);
|
||||
} else {
|
||||
center = pointFrom(x + width / 2, y + height / 2);
|
||||
center = elementCenterPoint(element);
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
|
@ -180,10 +180,17 @@ export class Pointer {
|
|||
public clientX = 0;
|
||||
public clientY = 0;
|
||||
|
||||
static activePointers: Pointer[] = [];
|
||||
static resetAll() {
|
||||
Pointer.activePointers.forEach((pointer) => pointer.reset());
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly pointerType: "mouse" | "touch" | "pen",
|
||||
private readonly pointerId = 1,
|
||||
) {}
|
||||
) {
|
||||
Pointer.activePointers.push(this);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clientX = 0;
|
||||
|
@ -402,7 +409,10 @@ const proxy = <T extends ExcalidrawElement>(
|
|||
};
|
||||
|
||||
/** Tools that can be used to draw shapes */
|
||||
type DrawingToolName = Exclude<ToolType, "lock" | "selection" | "eraser">;
|
||||
type DrawingToolName = Exclude<
|
||||
ToolType,
|
||||
"lock" | "selection" | "eraser" | "lasso"
|
||||
>;
|
||||
|
||||
type Element<T extends DrawingToolName> = T extends "line" | "freedraw"
|
||||
? ExcalidrawLinearElement
|
||||
|
|
1812
packages/excalidraw/tests/lasso.test.tsx
Normal file
1812
packages/excalidraw/tests/lasso.test.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,5 @@
|
|||
import { newArrowElement } from "@excalidraw/element/newElement";
|
||||
|
||||
import { pointCenter, pointFrom } from "@excalidraw/math";
|
||||
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
@ -19,7 +21,7 @@ import {
|
|||
import * as textElementUtils from "@excalidraw/element/textElement";
|
||||
import { wrapText } from "@excalidraw/element/textWrapping";
|
||||
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
|
@ -164,6 +166,24 @@ describe("Test Linear Elements", () => {
|
|||
Keyboard.keyPress(KEYS.DELETE);
|
||||
};
|
||||
|
||||
it("should normalize the element points at creation", () => {
|
||||
const element = newArrowElement({
|
||||
type: "arrow",
|
||||
points: [pointFrom<LocalPoint>(0.5, 0), pointFrom<LocalPoint>(100, 100)],
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(element.points).toEqual([
|
||||
pointFrom<LocalPoint>(0.5, 0),
|
||||
pointFrom<LocalPoint>(100, 100),
|
||||
]);
|
||||
new LinearElementEditor(element);
|
||||
expect(element.points).toEqual([
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(99.5, 100),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not drag line and add midpoint until dragged beyond a threshold", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
|
|
@ -19,7 +19,7 @@ import type { AllPossibleKeys } from "@excalidraw/common/utility-types";
|
|||
|
||||
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
||||
|
||||
import { UI } from "./helpers/ui";
|
||||
import { Pointer, UI } from "./helpers/ui";
|
||||
import * as toolQueries from "./queries/toolQueries";
|
||||
|
||||
import type { RenderResult, RenderOptions } from "@testing-library/react";
|
||||
|
@ -42,6 +42,10 @@ type TestRenderFn = (
|
|||
) => Promise<RenderResult<typeof customQueries>>;
|
||||
|
||||
const renderApp: TestRenderFn = async (ui, options) => {
|
||||
// when tests reuse Pointer instances let's reset the last
|
||||
// pointer poisitions so there's no leak between tests
|
||||
Pointer.resetAll();
|
||||
|
||||
if (options?.localStorageData) {
|
||||
initLocalStorage(options.localStorageData);
|
||||
delete options.localStorageData;
|
||||
|
|
|
@ -136,6 +136,7 @@ export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
|||
|
||||
export type ToolType =
|
||||
| "selection"
|
||||
| "lasso"
|
||||
| "rectangle"
|
||||
| "diamond"
|
||||
| "ellipse"
|
||||
|
@ -308,6 +309,8 @@ export interface AppState {
|
|||
*/
|
||||
lastActiveTool: ActiveTool | null;
|
||||
locked: boolean;
|
||||
// indicates if the current tool is temporarily switched on from the selection tool
|
||||
fromSelection: boolean;
|
||||
} & ActiveTool;
|
||||
penMode: boolean;
|
||||
penDetected: boolean;
|
||||
|
@ -598,6 +601,7 @@ export interface ExcalidrawProps {
|
|||
) => JSX.Element | null;
|
||||
aiEnabled?: boolean;
|
||||
showDeprecatedFonts?: boolean;
|
||||
renderScrollbars?: boolean;
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
|
@ -721,7 +725,8 @@ export type PointerDownState = Readonly<{
|
|||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
||||
// The previous pointer position
|
||||
lastCoords: { x: number; y: number };
|
||||
// map of original elements data
|
||||
// original element frozen snapshots so we can access the original
|
||||
// element attribute values at time of pointerdown
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||
resize: {
|
||||
// Handle when resizing, might change during the pointer interaction
|
||||
|
@ -755,6 +760,9 @@ export type PointerDownState = Readonly<{
|
|||
hasOccurred: boolean;
|
||||
// Might change during the pointer interaction
|
||||
offset: { x: number; y: number } | null;
|
||||
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||
// to current pointer position at time of duplication.
|
||||
origin: { x: number; y: number };
|
||||
};
|
||||
// We need to have these in the state so that we can unsubscribe them
|
||||
eventListeners: {
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
mockBoundingClientRect,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
} from "../tests/test-utils";
|
||||
import { actionBindText } from "../actions";
|
||||
|
||||
unmountComponent();
|
||||
|
||||
|
@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => {
|
|||
expect(text.containerId).toBe(null);
|
||||
expect(text.text).toBe("Excalidraw");
|
||||
});
|
||||
|
||||
it("should reset the text element angle to the container's when binding to rotated non-arrow container", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "Hello World!",
|
||||
angle: 45,
|
||||
});
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, text]);
|
||||
|
||||
API.setSelectedElements([rectangle, text]);
|
||||
|
||||
h.app.actionManager.executeAction(actionBindText);
|
||||
|
||||
expect(text.angle).toBe(30);
|
||||
expect(rectangle.angle).toBe(30);
|
||||
});
|
||||
|
||||
it("should reset the text element angle to 0 when binding to rotated arrow container", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "Hello World!",
|
||||
angle: 45,
|
||||
});
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([arrow, text]);
|
||||
|
||||
API.setSelectedElements([arrow, text]);
|
||||
|
||||
h.app.actionManager.executeAction(actionBindText);
|
||||
|
||||
expect(text.angle).toBe(0);
|
||||
expect(arrow.angle).toBe(30);
|
||||
});
|
||||
|
||||
it("should keep the text label at 0 degrees when used as an arrow label", async () => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([arrow]);
|
||||
API.setSelectedElements([arrow]);
|
||||
|
||||
mouse.doubleClickAt(
|
||||
arrow.x + arrow.width / 2,
|
||||
arrow.y + arrow.height / 2,
|
||||
);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
|
||||
expect(h.elements[1].angle).toBe(0);
|
||||
});
|
||||
|
||||
it("should keep the text label at the same degrees when used as a non-arrow label", async () => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
mouse.doubleClickAt(
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
|
||||
expect(h.elements[1].angle).toBe(30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue