mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' into fix/iTXt-png-chunks
This commit is contained in:
commit
7ddb09f12c
69 changed files with 3455 additions and 576 deletions
|
@ -226,8 +226,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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -139,7 +139,8 @@ export type ActionName =
|
|||
| "copyElementLink"
|
||||
| "linkToElement"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame";
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
|
|
@ -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 {
|
||||
|
@ -461,6 +462,8 @@ import { isOverScrollBars } from "../scene/scrollbars";
|
|||
|
||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||
|
||||
import { LassoTrail } from "../lasso";
|
||||
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
|
@ -692,6 +695,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
? "rgba(0, 0, 0, 0.2)"
|
||||
: "rgba(255, 255, 255, 0.2)",
|
||||
});
|
||||
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
|
||||
|
||||
onChangeEmitter = new Emitter<
|
||||
[
|
||||
|
@ -1670,7 +1674,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.eraserTrail,
|
||||
this.lassoTrail,
|
||||
]}
|
||||
/>
|
||||
{selectedElements.length === 1 &&
|
||||
this.state.openDialog?.name !==
|
||||
|
@ -4630,7 +4638,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);
|
||||
|
@ -4738,7 +4749,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(
|
||||
|
@ -4780,7 +4792,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,
|
||||
|
@ -6603,6 +6629,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";
|
||||
|
||||
|
@ -6610,7 +6637,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" ||
|
||||
|
@ -7067,7 +7100,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: {},
|
||||
|
@ -8267,7 +8303,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);
|
||||
|
||||
|
@ -8485,20 +8522,26 @@ 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 idsOfElementsToDuplicate = new Map(
|
||||
selectedElements.map((el) => [el.id, el]),
|
||||
);
|
||||
|
||||
const { newElements: clonedElements, elementsWithClones } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate: new Map(
|
||||
selectedElements.map((el) => [el.id, el]),
|
||||
),
|
||||
idsOfElementsToDuplicate,
|
||||
overrides: (el) => {
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
|
||||
|
@ -8506,6 +8549,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
seed: origEl.seed,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -8525,7 +8569,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const nextSceneElements = syncMovedIndices(
|
||||
mappedNewSceneElements || elementsWithClones,
|
||||
arrayToMap(clonedElements),
|
||||
);
|
||||
).map((el) => {
|
||||
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||
return newElementWith(el, {
|
||||
seed: randomInteger(),
|
||||
});
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(nextSceneElements);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
|
@ -8539,7 +8590,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!
|
||||
|
@ -8794,6 +8875,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);
|
||||
|
@ -9510,6 +9593,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
|
||||
|
@ -9608,7 +9693,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,
|
||||
|
@ -10463,7 +10554,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,
|
||||
});
|
||||
|
|
|
@ -27,16 +27,22 @@
|
|||
.color-picker__top-picks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-picker__button {
|
||||
--radius: 0.25rem;
|
||||
--radius: 4px;
|
||||
--size: 1.375rem;
|
||||
|
||||
&.has-outline {
|
||||
box-shadow: inset 0 0 0 1px #d9d9d9;
|
||||
}
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border: 1px solid var(--color-gray-30);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
filter: var(--theme-filter);
|
||||
background-color: var(--swatch-color);
|
||||
|
@ -45,16 +51,20 @@
|
|||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.active):not(.color-picker__button--large) {
|
||||
transform: scale(1.075);
|
||||
}
|
||||
|
||||
&:hover:not(.active).color-picker__button--large {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
box-shadow: 0 0 0 1px var(--color-gray-30);
|
||||
border-radius: calc(var(--radius) + 1px);
|
||||
border-radius: var(--radius);
|
||||
filter: var(--theme-filter);
|
||||
}
|
||||
}
|
||||
|
@ -62,13 +72,14 @@
|
|||
&.active {
|
||||
.color-picker__button-outline {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
--offset: -1px;
|
||||
top: var(--offset);
|
||||
left: var(--offset);
|
||||
right: var(--offset);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -123,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 =
|
||||
|
|
|
@ -315,6 +315,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) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
.range-wrapper {
|
||||
position: relative;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 30px;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.range-input {
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
.drag-input-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,24 +18,14 @@
|
|||
|
||||
.drag-input-label {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-right: 0;
|
||||
padding: 0 0.5rem 0 0.75rem;
|
||||
border: 0;
|
||||
padding: 0 0.5rem 0 0.25rem;
|
||||
min-width: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 2rem;
|
||||
box-sizing: border-box;
|
||||
box-sizing: content-box;
|
||||
color: var(--popup-text-color);
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
|
||||
border-right: 1px solid var(--default-border-color);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -51,20 +43,8 @@
|
|||
border: 0;
|
||||
outline: none;
|
||||
height: 2rem;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-left: 0;
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
|
||||
border-left: 1px solid var(--default-border-color);
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
padding: 0.5rem;
|
||||
padding-left: 0.25rem;
|
||||
appearance: none;
|
||||
|
|
|
@ -41,6 +41,10 @@
|
|||
div + div {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__row--heading {
|
||||
|
|
|
@ -289,7 +289,11 @@ export const StatsInner = memo(
|
|||
</StatsRow>
|
||||
)}
|
||||
|
||||
<StatsRow heading data-testid="stats-element-type">
|
||||
<StatsRow
|
||||
heading
|
||||
data-testid="stats-element-type"
|
||||
style={{ margin: "0.3125rem 0" }}
|
||||
>
|
||||
{appState.croppingElementId
|
||||
? t("labels.imageCropping")
|
||||
: t(`element.${singleElement.type}`)}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -86,6 +86,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||
boolean
|
||||
> = {
|
||||
selection: true,
|
||||
lasso: true,
|
||||
text: true,
|
||||
rectangle: true,
|
||||
diamond: true,
|
||||
|
@ -221,7 +222,7 @@ const restoreElementWithProperties = <
|
|||
"customData" in extra ? extra.customData : element.customData;
|
||||
}
|
||||
|
||||
return {
|
||||
const ret = {
|
||||
// spread the original element properties to not lose unknown ones
|
||||
// for forward-compatibility
|
||||
...element,
|
||||
|
@ -230,6 +231,12 @@ const restoreElementWithProperties = <
|
|||
...getNormalizedDimensions(base),
|
||||
...extra,
|
||||
} as unknown as T;
|
||||
|
||||
// strip legacy props (migrated in previous steps)
|
||||
delete ret.strokeSharpness;
|
||||
delete ret.boundElementIds;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const restoreElement = (
|
||||
|
|
|
@ -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,
|
||||
|
|
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;
|
||||
}
|
||||
}
|
107
packages/excalidraw/lasso/utils.ts
Normal file
107
packages/excalidraw/lasso/utils.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { simplify } from "points-on-curve";
|
||||
|
||||
import {
|
||||
polygonFromPoints,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
};
|
|
@ -276,6 +276,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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -20,7 +20,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 238820263,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
|
@ -54,14 +54,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 1505387817,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 23633383,
|
||||
"version": 6,
|
||||
"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",
|
||||
|
@ -2188,6 +2194,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 +2375,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 +2696,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 +2943,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 +3187,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 +3418,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 +3675,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 +3987,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 +4410,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 +4694,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 +4948,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 +5159,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 +5359,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 +5742,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 +6033,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 +6842,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 +7173,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 +7450,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 +7685,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 +7923,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 +8104,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 +8285,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 +8466,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 +8690,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 +8913,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 +9108,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 +9332,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 +9513,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 +9736,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 +9917,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 +10112,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 +10293,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",
|
||||
|
@ -10764,6 +10802,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 +11080,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 +11207,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 +11407,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 +11719,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 +12132,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 +12746,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 +12876,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 +13461,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 +13800,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 +14066,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 +14193,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 +14573,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 +14700,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||
"activeEmbeddable": null,
|
||||
"activeTool": {
|
||||
"customType": null,
|
||||
"fromSelection": false,
|
||||
"lastActiveTool": null,
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -402,7 +402,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
|
||||
|
|
1813
packages/excalidraw/tests/lasso.test.tsx
Normal file
1813
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue