mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Compare commits
18 Commits
v0.16.2
...
dwelle/pro
Author | SHA1 | Date | |
---|---|---|---|
cb2bf44997 | |||
c199686c05 | |||
2e61926a6b | |||
e921bfb1ae | |||
e6f74350ac | |||
fa33aa08ab | |||
8b838049df | |||
1f4f5e11ae | |||
12420592ef | |||
bfd318e765 | |||
6a821f3b76 | |||
84fd13e872 | |||
7d2b6f3374 | |||
ceb637f5ea | |||
4c35eba72d | |||
4765f5536e | |||
556175558a | |||
4db73a7f95 |
@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports:
|
||||
|
||||
## Excalidraw.com
|
||||
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features:
|
||||
|
||||
- 📡 PWA support (works offline).
|
||||
- 🤼 Real-time collaboration.
|
||||
|
@ -38,6 +38,7 @@ To render an item, its recommended to use `MainMenu.Item`.
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | :-: | :-: | --- |
|
||||
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
||||
| `selected` | `boolean` | No | `false` | Whether item is active |
|
||||
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
|
||||
@ -70,6 +71,7 @@ function App() {
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | :-: | :-: | --- |
|
||||
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
||||
| `selected` | `boolean` | No | `false` | Whether item is active |
|
||||
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
|
||||
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
||||
|
@ -30,6 +30,7 @@ All `props` are *optional*.
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
| [`activeTool`](#activeTool) | `object` | - | Set the active editor tool (forced) |
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
||||
@ -236,4 +237,19 @@ validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) =>
|
||||
|
||||
This is an optional property. By default we support a handful of well-known sites. You may allow additional sites or disallow the default ones by supplying a custom validator. If you pass `true`, all URLs will be allowed. You can also supply a list of hostnames, RegExp (or list of RegExp objects), or a function. If the function returns `undefined`, the built-in validator will be used.
|
||||
|
||||
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.
|
||||
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.
|
||||
|
||||
### activeTool
|
||||
|
||||
```ts
|
||||
activeTool?:
|
||||
| {
|
||||
type: ToolType;
|
||||
}
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
};
|
||||
```
|
||||
|
||||
Tool to be force-set as active. As long as the prop is set, the editor active tool will be set to this. Useful only in specific circumstances such as when UI is disabled and the editor should be limited to just one tool (such as a laser pointer).
|
@ -1,6 +1,6 @@
|
||||
# Customizing Styles
|
||||
|
||||
Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
||||
Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
||||
|
||||
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
|
||||
|
||||
|
@ -34,7 +34,7 @@ function App() {
|
||||
|
||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||
|
||||
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||
The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||
|
||||
```jsx showLineNumbers
|
||||
import { useState, useEffect } from "react";
|
||||
|
@ -15,7 +15,7 @@ In case you want to pick up something from the roadmap, comment on that issue an
|
||||
1. Run `yarn` to install dependencies
|
||||
1. Create a branch for your PR with `git checkout -b your-branch-name`
|
||||
|
||||
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
|
||||
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run:
|
||||
>
|
||||
> ```bash
|
||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
||||
|
@ -15,7 +15,7 @@ const FeatureList = [
|
||||
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Want to build your own app powered by Excalidraw by don't know where to
|
||||
Want to build your own app powered by Excalidraw but don't know where to
|
||||
start?
|
||||
</>
|
||||
),
|
||||
|
@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
|
||||
type: "MOUSE_LOCATION";
|
||||
payload: {
|
||||
socketId: string;
|
||||
pointer: { x: number; y: number };
|
||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||
button: "down" | "up";
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
username: string;
|
||||
|
@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/laser-pointer": "1.2.0",
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@radix-ui/react-popover": "1.0.3",
|
||||
"@radix-ui/react-tabs": "1.0.2",
|
||||
|
@ -11,7 +11,7 @@
|
||||
{
|
||||
"src": "apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
"sizes": "180x180"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
|
@ -90,7 +90,9 @@ export const actionFinalize = register({
|
||||
}
|
||||
}
|
||||
if (isInvisiblySmallElement(multiPointElement)) {
|
||||
newElements = newElements.slice(0, -1);
|
||||
newElements = newElements.filter(
|
||||
(el) => el.id !== multiPointElement.id,
|
||||
);
|
||||
}
|
||||
|
||||
// If the multi point line closes the loop,
|
||||
|
@ -15,6 +15,7 @@ export const actionToggleGridMode = register({
|
||||
appState: {
|
||||
...appState,
|
||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||
objectsSnapModeEnabled: false,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
28
src/actions/actionToggleObjectsSnapMode.tsx
Normal file
28
src/actions/actionToggleObjectsSnapMode.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleObjectsSnapMode = register({
|
||||
name: "objectsSnapMode",
|
||||
viewMode: true,
|
||||
trackEvent: {
|
||||
category: "canvas",
|
||||
predicate: (appState) => !appState.objectsSnapModeEnabled,
|
||||
},
|
||||
perform(elements, appState) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
objectsSnapModeEnabled: !this.checked!(appState),
|
||||
gridSize: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.objectsSnapModeEnabled,
|
||||
predicate: (elements, appState, appProps) => {
|
||||
return typeof appProps.objectsSnapModeEnabled === "undefined";
|
||||
},
|
||||
contextItemLabel: "buttons.objectsSnapMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
|
||||
});
|
@ -80,6 +80,7 @@ export {
|
||||
|
||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
|
||||
|
||||
export { actionToggleStats } from "./actionToggleStats";
|
||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||
|
@ -28,6 +28,7 @@ export type ShortcutName =
|
||||
| "ungroup"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "objectsSnapMode"
|
||||
| "stats"
|
||||
| "addToLibrary"
|
||||
| "viewMode"
|
||||
@ -74,6 +75,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||
zenMode: [getShortcutKey("Alt+Z")],
|
||||
objectsSnapMode: [getShortcutKey("Alt+S")],
|
||||
stats: [getShortcutKey("Alt+/")],
|
||||
addToLibrary: [],
|
||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||
|
@ -51,6 +51,7 @@ export type ActionName =
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "objectsSnapMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
|
@ -99,6 +99,12 @@ export const getDefaultAppState = (): Omit<
|
||||
pendingImageElementId: null,
|
||||
showHyperlinkPopup: false,
|
||||
selectedLinearElement: null,
|
||||
snapLines: [],
|
||||
originSnapOffset: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
objectsSnapModeEnabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
@ -206,6 +212,9 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
pendingImageElementId: { browser: false, export: false, server: false },
|
||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||
selectedLinearElement: { browser: true, export: false, server: false },
|
||||
snapLines: { browser: false, export: false, server: false },
|
||||
originSnapOffset: { browser: false, export: false, server: false },
|
||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
@ -14,13 +14,8 @@ import {
|
||||
hasText,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { UIAppState, Zoom } from "../types";
|
||||
import {
|
||||
capitalizeString,
|
||||
isTransparent,
|
||||
updateActiveTool,
|
||||
setCursorForShape,
|
||||
} from "../utils";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
@ -36,7 +31,12 @@ import {
|
||||
|
||||
import "./Actions.scss";
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||
import {
|
||||
EmbedIcon,
|
||||
extraToolsIcon,
|
||||
frameToolIcon,
|
||||
laserPointerToolIcon,
|
||||
} from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
@ -215,18 +215,23 @@ export const SelectedShapeActions = ({
|
||||
export const ShapesSwitcher = ({
|
||||
interactiveCanvas,
|
||||
activeTool,
|
||||
setAppState,
|
||||
onImageAction,
|
||||
appState,
|
||||
app,
|
||||
}: {
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
activeTool: UIAppState["activeTool"];
|
||||
setAppState: React.Component<any, UIAppState>["setState"];
|
||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||
appState: UIAppState;
|
||||
app: AppClassProperties;
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
const device = useDevice();
|
||||
|
||||
const frameToolSelected = activeTool.type === "frame";
|
||||
const laserToolSelected = activeTool.type === "laser";
|
||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||
|
||||
return (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
@ -251,29 +256,14 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-${value}`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
if (appState.activeTool.type !== value) {
|
||||
trackEvent("toolbar", value, "ui");
|
||||
}
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: value,
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
activeEmbeddable: null,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
setCursorForShape(interactiveCanvas, {
|
||||
...appState,
|
||||
activeTool: nextActiveTool,
|
||||
});
|
||||
app.setActiveTool({ type: value });
|
||||
if (value === "image") {
|
||||
onImageAction({ pointerType });
|
||||
}
|
||||
@ -300,24 +290,14 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-frame`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "frame", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
selected={activeTool.type === "frame"}
|
||||
/>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
@ -330,30 +310,28 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-embeddable`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "embeddable", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
app.setActiveTool({ type: "embeddable" });
|
||||
}}
|
||||
selected={activeTool.type === "embeddable"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className="App-toolbar__extra-tools-trigger"
|
||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||
"App-toolbar__extra-tools-trigger--selected":
|
||||
frameToolSelected ||
|
||||
embeddableToolSelected ||
|
||||
// in collab we're already highlighting the laser button
|
||||
// outside toolbar, so let's not highlight extra-tools button
|
||||
// on top of it
|
||||
(laserToolSelected && !app.props.isCollaborating),
|
||||
})}
|
||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||
title={t("toolBar.extraTools")}
|
||||
>
|
||||
@ -366,37 +344,36 @@ export const ShapesSwitcher = ({
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
icon={frameToolIcon}
|
||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid="toolbar-frame"
|
||||
selected={frameToolSelected}
|
||||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
app.setActiveTool({ type: "embeddable" });
|
||||
}}
|
||||
icon={EmbedIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
selected={embeddableToolSelected}
|
||||
>
|
||||
{t("toolBar.embeddable")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
app.setActiveTool({ type: "laser" });
|
||||
}}
|
||||
icon={laserPointerToolIcon}
|
||||
data-testid="toolbar-laser"
|
||||
selected={laserToolSelected}
|
||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
actionLink,
|
||||
actionToggleElementLock,
|
||||
actionToggleLinearEditor,
|
||||
actionToggleObjectsSnapMode,
|
||||
} from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
@ -210,7 +211,7 @@ import {
|
||||
import Scene from "../scene/Scene";
|
||||
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { findShapeByKey, SHAPES } from "../shapes";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
@ -228,6 +229,9 @@ import {
|
||||
FrameNameBoundsCache,
|
||||
SidebarName,
|
||||
SidebarTabName,
|
||||
KeyboardModifiersObject,
|
||||
CollaboratorPointer,
|
||||
ToolType,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
@ -342,6 +346,17 @@ import {
|
||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
snapDraggedElements,
|
||||
isActiveToolNonLinearSnappable,
|
||||
snapNewElement,
|
||||
snapResizingElements,
|
||||
isSnappingEnabled,
|
||||
getVisibleGaps,
|
||||
getReferenceSnapPoints,
|
||||
SnapCache,
|
||||
} from "../snapping";
|
||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||
@ -354,6 +369,8 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||
import { Renderer } from "../scene/Renderer";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
||||
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@ -478,10 +495,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement>();
|
||||
|
||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||
lastPointerDown: React.PointerEvent<HTMLElement> | null = null;
|
||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
||||
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
||||
null;
|
||||
lastViewportPosition = { x: 0, y: 0 };
|
||||
|
||||
laserPathManager: LaserPathManager = new LaserPathManager(this);
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
const defaultAppState = getDefaultAppState();
|
||||
@ -490,6 +510,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
viewModeEnabled = false,
|
||||
zenModeEnabled = false,
|
||||
gridModeEnabled = false,
|
||||
objectsSnapModeEnabled = false,
|
||||
theme = defaultAppState.theme,
|
||||
name = defaultAppState.name,
|
||||
} = props;
|
||||
@ -500,6 +521,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...this.getCanvasOffsets(),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
objectsSnapModeEnabled,
|
||||
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||
name,
|
||||
width: window.innerWidth,
|
||||
@ -1086,7 +1108,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
cursor: CURSOR_TYPE.MOVE,
|
||||
pointerEvents: this.state.viewModeEnabled
|
||||
? POINTER_EVENTS.disabled
|
||||
: POINTER_EVENTS.inheritFromUI,
|
||||
: POINTER_EVENTS.enabled,
|
||||
}}
|
||||
onPointerDown={(event) => this.handleCanvasPointerDown(event)}
|
||||
onWheel={(event) => this.handleWheel(event)}
|
||||
@ -1188,12 +1210,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
}
|
||||
app={this}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
<div className="excalidraw-eye-dropper-container" />
|
||||
<LaserToolOverlay manager={this.laserPathManager} />
|
||||
{selectedElements.length === 1 &&
|
||||
!this.state.contextMenu &&
|
||||
this.state.showHyperlinkPopup && (
|
||||
@ -1721,7 +1745,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.removeEventListeners();
|
||||
this.scene.destroy();
|
||||
this.library.destroy();
|
||||
this.laserPathManager.destroy();
|
||||
ShapeCache.destroy();
|
||||
SnapCache.destroy();
|
||||
clearTimeout(touchTimeout);
|
||||
isSomeElementSelected.clearCache();
|
||||
selectGroupsForSelectedElements.clearCache();
|
||||
@ -1877,6 +1903,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.activeTool &&
|
||||
this.props.activeTool.type !== this.state.activeTool.type
|
||||
) {
|
||||
this.setActiveTool(this.props.activeTool);
|
||||
}
|
||||
|
||||
if (
|
||||
prevState.scrollX !== this.state.scrollX ||
|
||||
prevState.scrollY !== this.state.scrollY
|
||||
@ -2536,10 +2569,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
togglePenMode = () => {
|
||||
togglePenMode = (force?: boolean) => {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
penMode: !prevState.penMode,
|
||||
penMode: force ?? !prevState.penMode,
|
||||
penDetected: true,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -3033,6 +3067,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
||||
if (this.state.activeTool.type === "laser") {
|
||||
this.setActiveTool({ type: "selection" });
|
||||
} else {
|
||||
this.setActiveTool({ type: "laser" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||
@ -3092,24 +3135,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
private setActiveTool = (
|
||||
setActiveTool = (
|
||||
tool:
|
||||
| {
|
||||
type:
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
| "embeddable";
|
||||
type: ToolType;
|
||||
}
|
||||
| { type: "custom"; customType: string },
|
||||
) => {
|
||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||
if (nextActiveTool.type === "hand") {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
} else if (!isHoldingSpace) {
|
||||
setCursorForShape(this.interactiveCanvas, this.state);
|
||||
}
|
||||
|
||||
if (isToolIcon(document.activeElement)) {
|
||||
this.focusContainer();
|
||||
}
|
||||
@ -3119,17 +3153,41 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (nextActiveTool.type === "image") {
|
||||
this.onImageAction();
|
||||
}
|
||||
if (nextActiveTool.type !== "selection") {
|
||||
this.setState({
|
||||
activeTool: nextActiveTool,
|
||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||
selectedGroupIds: {},
|
||||
editingGroupId: null,
|
||||
|
||||
this.setState((prevState) => {
|
||||
const commonResets = {
|
||||
snapLines: prevState.snapLines.length ? [] : prevState.snapLines,
|
||||
originSnapOffset: null,
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({ activeTool: nextActiveTool, activeEmbeddable: null });
|
||||
}
|
||||
} as const;
|
||||
let nextState: AppState;
|
||||
|
||||
if (nextActiveTool.type !== "selection") {
|
||||
nextState = {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
|
||||
editingGroupId: null,
|
||||
multiElement: null,
|
||||
...commonResets,
|
||||
};
|
||||
} else {
|
||||
nextState = {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
...commonResets,
|
||||
};
|
||||
}
|
||||
|
||||
if (nextActiveTool.type === "hand") {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
} else if (!isHoldingSpace) {
|
||||
setCursorForShape(this.interactiveCanvas, nextState);
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
};
|
||||
|
||||
private setCursor = (cursor: string) => {
|
||||
@ -3706,10 +3764,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isTouchScreen: boolean,
|
||||
) => {
|
||||
const draggedDistance = distance2d(
|
||||
this.lastPointerDown!.clientX,
|
||||
this.lastPointerDown!.clientY,
|
||||
this.lastPointerUp!.clientX,
|
||||
this.lastPointerUp!.clientY,
|
||||
this.lastPointerDownEvent!.clientX,
|
||||
this.lastPointerDownEvent!.clientY,
|
||||
this.lastPointerUpEvent!.clientX,
|
||||
this.lastPointerUpEvent!.clientY,
|
||||
);
|
||||
if (
|
||||
!this.hitLinkElement ||
|
||||
@ -3720,7 +3778,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerDown!,
|
||||
this.lastPointerDownEvent!,
|
||||
this.state,
|
||||
);
|
||||
const lastPointerDownHittingLinkIcon = isPointHittingLink(
|
||||
@ -3730,7 +3788,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.device.isMobile,
|
||||
);
|
||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerUp!,
|
||||
this.lastPointerUpEvent!,
|
||||
this.state,
|
||||
);
|
||||
const lastPointerUpHittingLinkIcon = isPointHittingLink(
|
||||
@ -3865,6 +3923,30 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
||||
|
||||
if (
|
||||
!this.state.draggingElement &&
|
||||
isActiveToolNonLinearSnappable(this.state.activeTool.type)
|
||||
) {
|
||||
const { originOffset, snapLines } = getSnapLinesAtPointer(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
{
|
||||
x: scenePointerX,
|
||||
y: scenePointerY,
|
||||
},
|
||||
event,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
snapLines,
|
||||
originSnapOffset: originOffset,
|
||||
});
|
||||
} else if (!this.state.draggingElement) {
|
||||
this.setState({
|
||||
snapLines: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.editingLinearElement &&
|
||||
!this.state.editingLinearElement.isDragging
|
||||
@ -4335,6 +4417,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ contextMenu: null });
|
||||
}
|
||||
|
||||
if (this.state.snapLines) {
|
||||
this.setAppState({ snapLines: [] });
|
||||
}
|
||||
|
||||
this.updateGestureOnPointerDown(event);
|
||||
|
||||
// if dragging element is freedraw and another pointerdown event occurs
|
||||
@ -4407,17 +4493,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPointerDown = event;
|
||||
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPointerDownEvent = event;
|
||||
|
||||
this.setState({
|
||||
lastPointerDownWith: event.pointerType,
|
||||
cursorButton: "down",
|
||||
});
|
||||
this.savePointer(event.clientX, event.clientY, "down");
|
||||
|
||||
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only handle left mouse button or touch
|
||||
if (
|
||||
event.button !== POINTER_BUTTON.MAIN &&
|
||||
@ -4508,6 +4595,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||
} else if (this.state.activeTool.type === "frame") {
|
||||
this.createFrameElementOnPointerDown(pointerDownState);
|
||||
} else if (this.state.activeTool.type === "laser") {
|
||||
this.laserPathManager.startPath(
|
||||
pointerDownState.lastCoords.x,
|
||||
pointerDownState.lastCoords.y,
|
||||
);
|
||||
} else if (
|
||||
this.state.activeTool.type !== "eraser" &&
|
||||
this.state.activeTool.type !== "hand"
|
||||
@ -4531,7 +4623,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
lastPointerUp = onPointerUp;
|
||||
|
||||
if (!this.state.viewModeEnabled) {
|
||||
if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") {
|
||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||
@ -4547,14 +4639,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
this.removePointer(event);
|
||||
this.lastPointerUp = event;
|
||||
this.lastPointerUpEvent = event;
|
||||
|
||||
const scenePointer = viewportCoordsToSceneCoords(
|
||||
{ clientX: event.clientX, clientY: event.clientY },
|
||||
this.state,
|
||||
);
|
||||
const clicklength =
|
||||
event.timeStamp - (this.lastPointerDown?.timeStamp ?? 0);
|
||||
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||
if (this.device.isMobile && clicklength < 300) {
|
||||
const hitElement = this.getElementAtPosition(
|
||||
scenePointer.x,
|
||||
@ -5308,7 +5400,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const embedLink = getEmbedLink(link);
|
||||
@ -5358,7 +5452,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||
@ -5535,7 +5631,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||
@ -5593,7 +5691,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
|
||||
? null
|
||||
: this.state.gridSize,
|
||||
);
|
||||
|
||||
const frame = newFrameElement({
|
||||
@ -5616,6 +5716,52 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
private maybeCacheReferenceSnapPoints(
|
||||
event: KeyboardModifiersObject,
|
||||
selectedElements: ExcalidrawElement[],
|
||||
recomputeAnyways: boolean = false,
|
||||
) {
|
||||
if (
|
||||
isSnappingEnabled({
|
||||
event,
|
||||
appState: this.state,
|
||||
selectedElements,
|
||||
}) &&
|
||||
(recomputeAnyways || !SnapCache.getReferenceSnapPoints())
|
||||
) {
|
||||
SnapCache.setReferenceSnapPoints(
|
||||
getReferenceSnapPoints(
|
||||
this.scene.getNonDeletedElements(),
|
||||
selectedElements,
|
||||
this.state,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeCacheVisibleGaps(
|
||||
event: KeyboardModifiersObject,
|
||||
selectedElements: ExcalidrawElement[],
|
||||
recomputeAnyways: boolean = false,
|
||||
) {
|
||||
if (
|
||||
isSnappingEnabled({
|
||||
event,
|
||||
appState: this.state,
|
||||
selectedElements,
|
||||
}) &&
|
||||
(recomputeAnyways || !SnapCache.getVisibleGaps())
|
||||
) {
|
||||
SnapCache.setVisibleGaps(
|
||||
getVisibleGaps(
|
||||
this.scene.getNonDeletedElements(),
|
||||
selectedElements,
|
||||
this.state,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDownFromPointerDownHandler(
|
||||
pointerDownState: PointerDownState,
|
||||
): (event: KeyboardEvent) => void {
|
||||
@ -5673,6 +5819,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeTool.type === "laser") {
|
||||
this.laserPathManager.addPointToPath(pointerCoords.x, pointerCoords.y);
|
||||
}
|
||||
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
@ -5845,33 +5995,62 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!this.state.editingElement &&
|
||||
this.state.activeEmbeddable?.state !== "active"
|
||||
) {
|
||||
const [dragX, dragY] = getGridPoint(
|
||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
);
|
||||
const dragOffset = {
|
||||
x: pointerCoords.x - pointerDownState.origin.x,
|
||||
y: pointerCoords.y - pointerDownState.origin.y,
|
||||
};
|
||||
|
||||
const [dragDistanceX, dragDistanceY] = [
|
||||
Math.abs(pointerCoords.x - pointerDownState.origin.x),
|
||||
Math.abs(pointerCoords.y - pointerDownState.origin.y),
|
||||
const originalElements = [
|
||||
...pointerDownState.originalElements.values(),
|
||||
];
|
||||
|
||||
// We only drag in one direction if shift is pressed
|
||||
const lockDirection = event.shiftKey;
|
||||
|
||||
if (lockDirection) {
|
||||
const distanceX = Math.abs(dragOffset.x);
|
||||
const distanceY = Math.abs(dragOffset.y);
|
||||
|
||||
const lockX = lockDirection && distanceX < distanceY;
|
||||
const lockY = lockDirection && distanceX > distanceY;
|
||||
|
||||
if (lockX) {
|
||||
dragOffset.x = 0;
|
||||
}
|
||||
|
||||
if (lockY) {
|
||||
dragOffset.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Snap cache *must* be synchronously popuplated before initial drag,
|
||||
// otherwise the first drag even will not snap, causing a jump before
|
||||
// it snaps to its position if previously snapped already.
|
||||
this.maybeCacheVisibleGaps(event, selectedElements);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements);
|
||||
|
||||
const { snapOffset, snapLines } = snapDraggedElements(
|
||||
getSelectedElements(originalElements, this.state),
|
||||
dragOffset,
|
||||
this.state,
|
||||
event,
|
||||
);
|
||||
|
||||
this.setState({ snapLines });
|
||||
|
||||
// when we're editing the name of a frame, we want the user to be
|
||||
// able to select and interact with the text input
|
||||
!this.state.editingFrame &&
|
||||
dragSelectedElements(
|
||||
pointerDownState,
|
||||
selectedElements,
|
||||
dragX,
|
||||
dragY,
|
||||
lockDirection,
|
||||
dragDistanceX,
|
||||
dragDistanceY,
|
||||
dragOffset,
|
||||
this.state,
|
||||
this.scene,
|
||||
snapOffset,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
);
|
||||
|
||||
this.maybeSuggestBindingForAll(selectedElements);
|
||||
|
||||
// We duplicate the selected element if alt is pressed on pointer move
|
||||
@ -5912,15 +6091,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
groupIdMap,
|
||||
element,
|
||||
);
|
||||
const [originDragX, originDragY] = getGridPoint(
|
||||
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
||||
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
);
|
||||
const origElement = pointerDownState.originalElements.get(
|
||||
element.id,
|
||||
)!;
|
||||
mutateElement(duplicatedElement, {
|
||||
x: duplicatedElement.x + (originDragX - dragX),
|
||||
y: duplicatedElement.y + (originDragY - dragY),
|
||||
x: origElement.x,
|
||||
y: origElement.y,
|
||||
});
|
||||
|
||||
// put duplicated element to pointerDownState.originalElements
|
||||
// so that we can snap to the duplicated element without releasing
|
||||
pointerDownState.originalElements.set(
|
||||
duplicatedElement.id,
|
||||
duplicatedElement,
|
||||
);
|
||||
|
||||
nextElements.push(duplicatedElement);
|
||||
elementsToAppend.push(element);
|
||||
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
||||
@ -5946,6 +6131,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
oldIdToDuplicatedId,
|
||||
);
|
||||
this.scene.replaceAllElements(nextSceneElements);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -6162,6 +6349,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isResizing,
|
||||
isRotating,
|
||||
} = this.state;
|
||||
|
||||
this.setState({
|
||||
isResizing: false,
|
||||
isRotating: false,
|
||||
@ -6176,8 +6364,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
multiElement || isTextElement(this.state.editingElement)
|
||||
? this.state.editingElement
|
||||
: null,
|
||||
snapLines: [],
|
||||
|
||||
originSnapOffset: null,
|
||||
});
|
||||
|
||||
SnapCache.setReferenceSnapPoints(null);
|
||||
SnapCache.setVisibleGaps(null);
|
||||
|
||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||
|
||||
this.setState({
|
||||
@ -6401,7 +6595,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
// remove invisible element which was added in onPointerDown
|
||||
this.scene.replaceAllElements(
|
||||
this.scene.getElementsIncludingDeleted().slice(0, -1),
|
||||
this.scene
|
||||
.getElementsIncludingDeleted()
|
||||
.filter((el) => el.id !== draggingElement.id),
|
||||
);
|
||||
this.setState({
|
||||
draggingElement: null,
|
||||
@ -6623,17 +6819,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
if (isEraserActive(this.state)) {
|
||||
const draggedDistance = distance2d(
|
||||
this.lastPointerDown!.clientX,
|
||||
this.lastPointerDown!.clientY,
|
||||
this.lastPointerUp!.clientX,
|
||||
this.lastPointerUp!.clientY,
|
||||
this.lastPointerDownEvent!.clientX,
|
||||
this.lastPointerDownEvent!.clientY,
|
||||
this.lastPointerUpEvent!.clientX,
|
||||
this.lastPointerUpEvent!.clientY,
|
||||
);
|
||||
|
||||
if (draggedDistance === 0) {
|
||||
const scenePointer = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: this.lastPointerUp!.clientX,
|
||||
clientY: this.lastPointerUp!.clientY,
|
||||
clientX: this.lastPointerUpEvent!.clientX,
|
||||
clientY: this.lastPointerUpEvent!.clientY,
|
||||
},
|
||||
this.state,
|
||||
);
|
||||
@ -6873,6 +7069,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
|
||||
}
|
||||
|
||||
if (activeTool.type === "laser") {
|
||||
this.laserPathManager.endPath();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||
resetCursor(this.interactiveCanvas);
|
||||
this.setState({
|
||||
@ -6889,14 +7090,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (
|
||||
hitElement &&
|
||||
this.lastPointerUp &&
|
||||
this.lastPointerDown &&
|
||||
this.lastPointerUp.timeStamp - this.lastPointerDown.timeStamp < 300 &&
|
||||
this.lastPointerUpEvent &&
|
||||
this.lastPointerDownEvent &&
|
||||
this.lastPointerUpEvent.timeStamp -
|
||||
this.lastPointerDownEvent.timeStamp <
|
||||
300 &&
|
||||
gesture.pointers.size <= 1 &&
|
||||
isEmbeddableElement(hitElement) &&
|
||||
this.isEmbeddableCenter(
|
||||
hitElement,
|
||||
this.lastPointerUp,
|
||||
this.lastPointerUpEvent,
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
)
|
||||
@ -7705,7 +7908,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
shouldResizeFromCenter(event),
|
||||
);
|
||||
} else {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
let [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
@ -7719,6 +7922,33 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? image.width / image.height
|
||||
: null;
|
||||
|
||||
this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
|
||||
|
||||
const { snapOffset, snapLines } = snapNewElement(
|
||||
draggingElement,
|
||||
this.state,
|
||||
event,
|
||||
{
|
||||
x:
|
||||
pointerDownState.originInGrid.x +
|
||||
(this.state.originSnapOffset?.x ?? 0),
|
||||
y:
|
||||
pointerDownState.originInGrid.y +
|
||||
(this.state.originSnapOffset?.y ?? 0),
|
||||
},
|
||||
{
|
||||
x: gridX - pointerDownState.originInGrid.x,
|
||||
y: gridY - pointerDownState.originInGrid.y,
|
||||
},
|
||||
);
|
||||
|
||||
gridX += snapOffset.x;
|
||||
gridY += snapOffset.y;
|
||||
|
||||
this.setState({
|
||||
snapLines,
|
||||
});
|
||||
|
||||
dragNewElement(
|
||||
draggingElement,
|
||||
this.state.activeTool.type,
|
||||
@ -7733,6 +7963,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter(event),
|
||||
aspectRatio,
|
||||
this.state.originSnapOffset,
|
||||
);
|
||||
|
||||
this.maybeSuggestBindingForAll([draggingElement]);
|
||||
@ -7774,7 +8005,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
const pointerCoords = pointerDownState.lastCoords;
|
||||
const [resizeX, resizeY] = getGridPoint(
|
||||
let [resizeX, resizeY] = getGridPoint(
|
||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
@ -7802,6 +8033,41 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
});
|
||||
|
||||
// check needed for avoiding flickering when a key gets pressed
|
||||
// during dragging
|
||||
if (!this.state.selectedElementsAreBeingDragged) {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||
);
|
||||
|
||||
const dragOffset = {
|
||||
x: gridX - pointerDownState.originInGrid.x,
|
||||
y: gridY - pointerDownState.originInGrid.y,
|
||||
};
|
||||
|
||||
const originalElements = [...pointerDownState.originalElements.values()];
|
||||
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements);
|
||||
|
||||
const { snapOffset, snapLines } = snapResizingElements(
|
||||
selectedElements,
|
||||
getSelectedElements(originalElements, this.state),
|
||||
this.state,
|
||||
event,
|
||||
dragOffset,
|
||||
transformHandleType,
|
||||
);
|
||||
|
||||
resizeX += snapOffset.x;
|
||||
resizeY += snapOffset.y;
|
||||
|
||||
this.setState({
|
||||
snapLines,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
transformElements(
|
||||
pointerDownState,
|
||||
@ -7817,6 +8083,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
resizeY,
|
||||
pointerDownState.resize.center.x,
|
||||
pointerDownState.resize.center.y,
|
||||
this.state,
|
||||
)
|
||||
) {
|
||||
this.maybeSuggestBindingForAll(selectedElements);
|
||||
@ -7904,6 +8171,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
actionUnlockAllElements,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionToggleGridMode,
|
||||
actionToggleObjectsSnapMode,
|
||||
actionToggleZenMode,
|
||||
actionToggleViewMode,
|
||||
actionToggleStats,
|
||||
@ -8050,15 +8318,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (!x || !y) {
|
||||
return;
|
||||
}
|
||||
const pointer = viewportCoordsToSceneCoords(
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{ clientX: x, clientY: y },
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (isNaN(pointer.x) || isNaN(pointer.y)) {
|
||||
if (isNaN(sceneX) || isNaN(sceneY)) {
|
||||
// sometimes the pointer goes off screen
|
||||
}
|
||||
|
||||
const pointer: CollaboratorPointer = {
|
||||
x: sceneX,
|
||||
y: sceneY,
|
||||
tool: this.state.activeTool.type === "laser" ? "laser" : "pointer",
|
||||
};
|
||||
|
||||
this.props.onPointerUpdate?.({
|
||||
pointer,
|
||||
button,
|
||||
|
@ -165,6 +165,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
||||
<Shortcut label={t("toolBar.laser")} shortcuts={[KEYS.K]} />
|
||||
<Shortcut
|
||||
label={t("labels.eyeDropper")}
|
||||
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
||||
@ -258,6 +259,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("buttons.zenMode")}
|
||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("buttons.objectsSnapMode")}
|
||||
shortcuts={[getShortcutKey("Alt+S")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.showGrid")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||
|
293
src/components/LaserTool/LaserPathManager.ts
Normal file
293
src/components/LaserTool/LaserPathManager.ts
Normal file
@ -0,0 +1,293 @@
|
||||
import { LaserPointer } from "@excalidraw/laser-pointer";
|
||||
|
||||
import { sceneCoordsToViewportCoords } from "../../utils";
|
||||
import App from "../App";
|
||||
import { getClientColor } from "../../clients";
|
||||
|
||||
// decay time in milliseconds
|
||||
const DECAY_TIME = 1000;
|
||||
// length of line in points before it starts decaying
|
||||
const DECAY_LENGTH = 50;
|
||||
|
||||
const average = (a: number, b: number) => (a + b) / 2;
|
||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||
const len = points.length;
|
||||
|
||||
if (len < 4) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
let a = points[0];
|
||||
let b = points[1];
|
||||
const c = points[2];
|
||||
|
||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
|
||||
2,
|
||||
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
|
||||
b[1],
|
||||
c[1],
|
||||
).toFixed(2)} T`;
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i];
|
||||
b = points[i + 1];
|
||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
|
||||
2,
|
||||
)} `;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += "Z";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
LPM: LaserPathManager;
|
||||
}
|
||||
}
|
||||
|
||||
function easeOutCubic(t: number) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function instantiateCollabolatorState(): CollabolatorState {
|
||||
return {
|
||||
currentPath: undefined,
|
||||
finishedPaths: [],
|
||||
lastPoint: [-10000, -10000],
|
||||
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
|
||||
};
|
||||
}
|
||||
|
||||
function instantiatePath() {
|
||||
LaserPointer.constants.cornerDetectionMaxAngle = 70;
|
||||
|
||||
return new LaserPointer({
|
||||
simplify: 0,
|
||||
streamline: 0.4,
|
||||
sizeMapping: (c) => {
|
||||
const pt = DECAY_TIME;
|
||||
const pl = DECAY_LENGTH;
|
||||
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
|
||||
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
|
||||
|
||||
return Math.min(easeOutCubic(l), easeOutCubic(t));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type CollabolatorState = {
|
||||
currentPath: LaserPointer | undefined;
|
||||
finishedPaths: LaserPointer[];
|
||||
lastPoint: [number, number];
|
||||
svg: SVGPathElement;
|
||||
};
|
||||
|
||||
export class LaserPathManager {
|
||||
private ownState: CollabolatorState;
|
||||
private collaboratorsState: Map<string, CollabolatorState> = new Map();
|
||||
|
||||
private rafId: number | undefined;
|
||||
private lastUpdate = 0;
|
||||
private container: SVGSVGElement | undefined;
|
||||
|
||||
constructor(private app: App) {
|
||||
this.ownState = instantiateCollabolatorState();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stop();
|
||||
this.lastUpdate = 0;
|
||||
this.ownState = instantiateCollabolatorState();
|
||||
this.collaboratorsState = new Map();
|
||||
}
|
||||
|
||||
startPath(x: number, y: number) {
|
||||
this.ownState.currentPath = instantiatePath();
|
||||
this.ownState.currentPath.addPoint([x, y, performance.now()]);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
|
||||
addPointToPath(x: number, y: number) {
|
||||
if (this.ownState.currentPath) {
|
||||
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
}
|
||||
|
||||
endPath() {
|
||||
if (this.ownState.currentPath) {
|
||||
this.ownState.currentPath.close();
|
||||
this.ownState.finishedPaths.push(this.ownState.currentPath);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
}
|
||||
|
||||
private updatePath(state: CollabolatorState) {
|
||||
this.lastUpdate = performance.now();
|
||||
|
||||
if (!this.isRunning) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
private isRunning = false;
|
||||
|
||||
start(svg?: SVGSVGElement) {
|
||||
if (svg) {
|
||||
this.container = svg;
|
||||
this.container.appendChild(this.ownState.svg);
|
||||
}
|
||||
|
||||
this.stop();
|
||||
this.isRunning = true;
|
||||
this.loop();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
this.rafId = undefined;
|
||||
}
|
||||
|
||||
loop() {
|
||||
this.rafId = requestAnimationFrame(this.loop.bind(this));
|
||||
|
||||
this.updateCollabolatorsState();
|
||||
|
||||
if (performance.now() - this.lastUpdate < DECAY_TIME * 2) {
|
||||
this.update();
|
||||
} else {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
draw(path: LaserPointer) {
|
||||
const stroke = path
|
||||
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
|
||||
.map(([x, y]) => {
|
||||
const result = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x, sceneY: y },
|
||||
this.app.state,
|
||||
);
|
||||
|
||||
return [result.x, result.y];
|
||||
});
|
||||
|
||||
return getSvgPathFromStroke(stroke, true);
|
||||
}
|
||||
|
||||
updateCollabolatorsState() {
|
||||
if (!this.container || !this.app.state.collaborators.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
|
||||
if (!this.collaboratorsState.has(key)) {
|
||||
const state = instantiateCollabolatorState();
|
||||
this.container.appendChild(state.svg);
|
||||
this.collaboratorsState.set(key, state);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
|
||||
const state = this.collaboratorsState.get(key)!;
|
||||
|
||||
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
|
||||
if (collabolator.button === "down" && state.currentPath === undefined) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath = instantiatePath();
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
|
||||
if (collabolator.button === "down" && state.currentPath !== undefined) {
|
||||
if (
|
||||
collabolator.pointer.x !== state.lastPoint[0] ||
|
||||
collabolator.pointer.y !== state.lastPoint[1]
|
||||
) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
}
|
||||
|
||||
if (collabolator.button === "up" && state.currentPath !== undefined) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
state.currentPath.close();
|
||||
|
||||
state.finishedPaths.push(state.currentPath);
|
||||
state.currentPath = undefined;
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, state] of this.collaboratorsState.entries()) {
|
||||
if (!this.app.state.collaborators.has(key)) {
|
||||
state.svg.remove();
|
||||
this.collaboratorsState.delete(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.finishedPaths = state.finishedPaths.filter((path) => {
|
||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
||||
|
||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
||||
});
|
||||
|
||||
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
|
||||
|
||||
if (state.currentPath) {
|
||||
paths += ` ${this.draw(state.currentPath)}`;
|
||||
}
|
||||
|
||||
state.svg.setAttribute("d", paths);
|
||||
state.svg.setAttribute("fill", getClientColor(key));
|
||||
}
|
||||
|
||||
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
|
||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
||||
|
||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
||||
});
|
||||
|
||||
let paths = this.ownState.finishedPaths
|
||||
.map((path) => this.draw(path))
|
||||
.join(" ");
|
||||
|
||||
if (this.ownState.currentPath) {
|
||||
paths += ` ${this.draw(this.ownState.currentPath)}`;
|
||||
}
|
||||
|
||||
this.ownState.svg.setAttribute("d", paths);
|
||||
this.ownState.svg.setAttribute("fill", "red");
|
||||
}
|
||||
}
|
41
src/components/LaserTool/LaserPointerButton.tsx
Normal file
41
src/components/LaserTool/LaserPointerButton.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import "../ToolIcon.scss";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ToolButtonSize } from "../ToolButton";
|
||||
import { laserPointerToolIcon } from "../icons";
|
||||
|
||||
type LaserPointerIconProps = {
|
||||
title?: string;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE: ToolButtonSize = "small";
|
||||
|
||||
export const LaserPointerButton = (props: LaserPointerIconProps) => {
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
"ToolIcon ToolIcon__LaserPointer",
|
||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||
{
|
||||
"is-mobile": props.isMobile,
|
||||
},
|
||||
)}
|
||||
title={`${props.title}`}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
name={props.name}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked}
|
||||
aria-label={props.title}
|
||||
data-testid="toolbar-LaserPointer"
|
||||
/>
|
||||
<div className="ToolIcon__icon">{laserPointerToolIcon}</div>
|
||||
</label>
|
||||
);
|
||||
};
|
27
src/components/LaserTool/LaserTool.tsx
Normal file
27
src/components/LaserTool/LaserTool.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { LaserPathManager } from "./LaserPathManager";
|
||||
import "./LaserToolOverlay.scss";
|
||||
|
||||
type LaserToolOverlayProps = {
|
||||
manager: LaserPathManager;
|
||||
};
|
||||
|
||||
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (svgRef.current) {
|
||||
manager.start(svgRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
manager.stop();
|
||||
};
|
||||
}, [manager]);
|
||||
|
||||
return (
|
||||
<div className="LaserToolOverlay">
|
||||
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
|
||||
</div>
|
||||
);
|
||||
};
|
20
src/components/LaserTool/LaserToolOverlay.scss
Normal file
20
src/components/LaserTool/LaserToolOverlay.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.excalidraw {
|
||||
.LaserToolOverlay {
|
||||
pointer-events: none;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
.LaserToolOverlayCanvas {
|
||||
image-rendering: auto;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ import "./Toolbar.scss";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import Scene from "../scene/Scene";
|
||||
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@ -77,6 +78,7 @@ interface LayerUIProps {
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
app: AppClassProperties;
|
||||
isCollaborating: boolean;
|
||||
}
|
||||
|
||||
const DefaultMainMenu: React.FC<{
|
||||
@ -134,6 +136,7 @@ const LayerUI = ({
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
app,
|
||||
isCollaborating,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
@ -279,7 +282,7 @@ const LayerUI = ({
|
||||
appState={appState}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
app={app}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
@ -288,6 +291,24 @@ const LayerUI = ({
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
{isCollaborating && (
|
||||
<Island
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
alignSelf: "center",
|
||||
height: "fit-content",
|
||||
}}
|
||||
>
|
||||
<LaserPointerButton
|
||||
title={t("toolBar.laser")}
|
||||
checked={appState.activeTool.type === "laser"}
|
||||
onChange={() =>
|
||||
app.setActiveTool({ type: "laser" })
|
||||
}
|
||||
isMobile
|
||||
/>
|
||||
</Island>
|
||||
)}
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
|
@ -87,7 +87,7 @@ export const MobileMenu = ({
|
||||
appState={appState}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
app={app}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
|
@ -170,5 +170,10 @@
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__LaserPointer .ToolIcon__icon {
|
||||
width: var(--default-button-size);
|
||||
height: var(--default-button-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,12 @@
|
||||
box-shadow: 0 0 0 1px
|
||||
var(--button-active-border, var(--color-primary-darkest)) inset;
|
||||
}
|
||||
|
||||
&--selected,
|
||||
&--selected:hover {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.App-toolbar__extra-tools-dropdown {
|
||||
|
@ -193,6 +193,8 @@ const getRelevantAppStateProps = (
|
||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||
activeEmbeddable: appState.activeEmbeddable,
|
||||
snapLines: appState.snapLines,
|
||||
zenModeEnabled: appState.zenModeEnabled,
|
||||
});
|
||||
|
||||
const areEqual = (
|
||||
|
@ -59,6 +59,11 @@
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: var(--color-primary-light);
|
||||
--icon-fill-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
@ -11,12 +11,14 @@ const DropdownMenuItem = ({
|
||||
children,
|
||||
shortcut,
|
||||
className,
|
||||
selected,
|
||||
...rest
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
onSelect: (event: Event) => void;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
selected?: boolean;
|
||||
className?: string;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
@ -26,7 +28,7 @@ const DropdownMenuItem = ({
|
||||
{...rest}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
|
@ -3,15 +3,19 @@ import React from "react";
|
||||
const DropdownMenuItemCustom = ({
|
||||
children,
|
||||
className = "",
|
||||
selected,
|
||||
...rest
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${
|
||||
selected ? `dropdown-menu-item--selected` : ``
|
||||
}`.trim()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ const DropdownMenuItemLink = ({
|
||||
children,
|
||||
onSelect,
|
||||
className = "",
|
||||
selected,
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
@ -19,6 +20,7 @@ const DropdownMenuItemLink = ({
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
onSelect?: (event: Event) => void;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
@ -29,7 +31,7 @@ const DropdownMenuItemLink = ({
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
@ -6,8 +6,13 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
||||
onSelect?: (event: Event) => void;
|
||||
}>({});
|
||||
|
||||
export const getDropdownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
export const getDropdownMenuItemClassName = (
|
||||
className = "",
|
||||
selected = false,
|
||||
) => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
||||
selected ? "dropdown-menu-item--selected" : ""
|
||||
}`.trim();
|
||||
};
|
||||
|
||||
export const useHandleDropdownMenuItemClick = (
|
||||
|
@ -13,7 +13,7 @@ import clsx from "clsx";
|
||||
import { Theme } from "../element/types";
|
||||
import { THEME } from "../constants";
|
||||
|
||||
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
|
||||
const handlerColor = (theme: Theme) =>
|
||||
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||
@ -1653,3 +1653,22 @@ export const frameToolIcon = createIcon(
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const laserPointerToolIcon = createIcon(
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="rotate(90 10 10)"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="m9.644 13.69 7.774-7.773a2.357 2.357 0 0 0-3.334-3.334l-7.773 7.774L8 12l1.643 1.69Z"
|
||||
/>
|
||||
<path d="m13.25 3.417 3.333 3.333M10 10l2-2M5 15l3-3M2.156 17.894l1-1M5.453 19.029l-.144-1.407M2.377 11.887l.866 1.118M8.354 17.273l-1.194-.758M.953 14.652l1.408.13" />
|
||||
</g>,
|
||||
|
||||
20,
|
||||
);
|
||||
|
@ -67,6 +67,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||
frame: true,
|
||||
embeddable: true,
|
||||
hand: true,
|
||||
laser: false,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
|
@ -158,7 +158,7 @@ export const getElementAbsoluteCoords = (
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
/*
|
||||
* for a given element, `getElementLineSegments` returns line segments
|
||||
* that can be used for visual collision detection (useful for frames)
|
||||
* as opposed to bounding box collision detection
|
||||
@ -674,6 +674,19 @@ export const getCommonBounds = (
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
export const getDraggedElementsBounds = (
|
||||
elements: ExcalidrawElement[],
|
||||
dragOffset: { x: number; y: number },
|
||||
) => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||
return [
|
||||
minX + dragOffset.x,
|
||||
minY + dragOffset.y,
|
||||
maxX + dragOffset.x,
|
||||
maxY + dragOffset.y,
|
||||
];
|
||||
};
|
||||
|
||||
export const getResizedElementAbsoluteCoords = (
|
||||
element: ExcalidrawElement,
|
||||
nextWidth: number,
|
||||
|
@ -6,23 +6,22 @@ import { NonDeletedExcalidrawElement } from "./types";
|
||||
import { AppState, PointerDownState } from "../types";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { isSelectedViaGroup } from "../groups";
|
||||
import { getGridPoint } from "../math";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isFrameElement } from "./typeChecks";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
lockDirection: boolean = false,
|
||||
distanceX: number = 0,
|
||||
distanceY: number = 0,
|
||||
offset: { x: number; y: number },
|
||||
appState: AppState,
|
||||
scene: Scene,
|
||||
snapOffset: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
gridSize: AppState["gridSize"],
|
||||
) => {
|
||||
const [x1, y1] = getCommonBounds(selectedElements);
|
||||
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
||||
|
||||
// we do not want a frame and its elements to be selected at the same time
|
||||
// but when it happens (due to some bug), we want to avoid updating element
|
||||
// in the frame twice, hence the use of set
|
||||
@ -44,12 +43,11 @@ export const dragSelectedElements = (
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(
|
||||
lockDirection,
|
||||
distanceX,
|
||||
distanceY,
|
||||
pointerDownState,
|
||||
element,
|
||||
offset,
|
||||
snapOffset,
|
||||
gridSize,
|
||||
);
|
||||
// update coords of bound text only if we're dragging the container directly
|
||||
// (we don't drag the group that it's part of)
|
||||
@ -69,12 +67,11 @@ export const dragSelectedElements = (
|
||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
||||
) {
|
||||
updateElementCoords(
|
||||
lockDirection,
|
||||
distanceX,
|
||||
distanceY,
|
||||
pointerDownState,
|
||||
textElement,
|
||||
offset,
|
||||
snapOffset,
|
||||
gridSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -85,31 +82,40 @@ export const dragSelectedElements = (
|
||||
};
|
||||
|
||||
const updateElementCoords = (
|
||||
lockDirection: boolean,
|
||||
distanceX: number,
|
||||
distanceY: number,
|
||||
pointerDownState: PointerDownState,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
offset: { x: number; y: number },
|
||||
dragOffset: { x: number; y: number },
|
||||
snapOffset: { x: number; y: number },
|
||||
gridSize: AppState["gridSize"],
|
||||
) => {
|
||||
let x: number;
|
||||
let y: number;
|
||||
if (lockDirection) {
|
||||
const lockX = lockDirection && distanceX < distanceY;
|
||||
const lockY = lockDirection && distanceX > distanceY;
|
||||
const original = pointerDownState.originalElements.get(element.id);
|
||||
x = lockX && original ? original.x : element.x + offset.x;
|
||||
y = lockY && original ? original.y : element.y + offset.y;
|
||||
} else {
|
||||
x = element.x + offset.x;
|
||||
y = element.y + offset.y;
|
||||
const originalElement =
|
||||
pointerDownState.originalElements.get(element.id) ?? element;
|
||||
|
||||
let nextX = originalElement.x + dragOffset.x + snapOffset.x;
|
||||
let nextY = originalElement.y + dragOffset.y + snapOffset.y;
|
||||
|
||||
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
||||
const [nextGridX, nextGridY] = getGridPoint(
|
||||
originalElement.x + dragOffset.x,
|
||||
originalElement.y + dragOffset.y,
|
||||
gridSize,
|
||||
);
|
||||
|
||||
if (snapOffset.x === 0) {
|
||||
nextX = nextGridX;
|
||||
}
|
||||
|
||||
if (snapOffset.y === 0) {
|
||||
nextY = nextGridY;
|
||||
}
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
x,
|
||||
y,
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDragOffsetXY = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
x: number,
|
||||
@ -133,6 +139,10 @@ export const dragNewElement = (
|
||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||
true */
|
||||
widthAspectRatio?: number | null,
|
||||
originOffset: {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null = null,
|
||||
) => {
|
||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||
if (widthAspectRatio) {
|
||||
@ -173,8 +183,8 @@ export const dragNewElement = (
|
||||
|
||||
if (width !== 0 && height !== 0) {
|
||||
mutateElement(draggingElement, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
y: newY + (originOffset?.y ?? 0),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ import {
|
||||
MaybeTransformHandleType,
|
||||
TransformHandleDirection,
|
||||
} from "./transformHandles";
|
||||
import { Point, PointerDownState } from "../types";
|
||||
import { AppState, Point, PointerDownState } from "../types";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getApproxMinLineWidth,
|
||||
@ -79,6 +79,7 @@ export const transformElements = (
|
||||
pointerY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
@ -466,8 +467,8 @@ export const resizeSingleElement = (
|
||||
boundTextElement.fontSize,
|
||||
boundTextElement.lineHeight,
|
||||
);
|
||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||
eleNewWidth = Math.max(eleNewWidth, minWidth);
|
||||
eleNewHeight = Math.max(eleNewHeight, minHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -508,8 +509,11 @@ export const resizeSingleElement = (
|
||||
}
|
||||
}
|
||||
|
||||
const flipX = eleNewWidth < 0;
|
||||
const flipY = eleNewHeight < 0;
|
||||
|
||||
// Flip horizontally
|
||||
if (eleNewWidth < 0) {
|
||||
if (flipX) {
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
||||
}
|
||||
@ -517,8 +521,9 @@ export const resizeSingleElement = (
|
||||
newTopLeft[0] += Math.abs(newBoundsWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Flip vertically
|
||||
if (eleNewHeight < 0) {
|
||||
if (flipY) {
|
||||
if (transformHandleDirection.includes("s")) {
|
||||
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
||||
}
|
||||
@ -542,10 +547,20 @@ export const resizeSingleElement = (
|
||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||
|
||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||
// So we need to readjust (x,y) to be where the first point should be
|
||||
const newOrigin = [...newTopLeft];
|
||||
const linearElementXOffset = stateAtResizeStart.x - newBoundsX1;
|
||||
const linearElementYOffset = stateAtResizeStart.y - newBoundsY1;
|
||||
newOrigin[0] += linearElementXOffset;
|
||||
newOrigin[1] += linearElementYOffset;
|
||||
|
||||
const nextX = newOrigin[0];
|
||||
const nextY = newOrigin[1];
|
||||
|
||||
// Readjust points for linear elements
|
||||
let rescaledElementPointsY;
|
||||
let rescaledPoints;
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
rescaledElementPointsY = rescalePoints(
|
||||
1,
|
||||
@ -562,16 +577,11 @@ export const resizeSingleElement = (
|
||||
);
|
||||
}
|
||||
|
||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||
// So we need to readjust (x,y) to be where the first point should be
|
||||
const newOrigin = [...newTopLeft];
|
||||
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
|
||||
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
|
||||
const resizedElement = {
|
||||
width: Math.abs(eleNewWidth),
|
||||
height: Math.abs(eleNewHeight),
|
||||
x: newOrigin[0],
|
||||
y: newOrigin[1],
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
points: rescaledPoints,
|
||||
};
|
||||
|
||||
@ -680,6 +690,10 @@ export const resizeMultipleElements = (
|
||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
||||
);
|
||||
|
||||
// const originalHeight = maxY - minY;
|
||||
// const originalWidth = maxX - minX;
|
||||
|
||||
const direction = transformHandleType;
|
||||
|
||||
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
||||
|
@ -12,6 +12,7 @@ export const showSelectedShapeActions = (
|
||||
(appState.editingElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand"))) ||
|
||||
appState.activeTool.type !== "hand" &&
|
||||
appState.activeTool.type !== "laser"))) ||
|
||||
getSelectedElements(elements, appState).length),
|
||||
);
|
||||
|
@ -957,7 +957,7 @@ describe("textWysiwyg", () => {
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
[
|
||||
85,
|
||||
4.5,
|
||||
4.999999999999986,
|
||||
]
|
||||
`);
|
||||
|
||||
@ -1002,8 +1002,8 @@ describe("textWysiwyg", () => {
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
[
|
||||
375,
|
||||
-539,
|
||||
374.99999999999994,
|
||||
-535.0000000000001,
|
||||
]
|
||||
`);
|
||||
});
|
||||
@ -1190,7 +1190,7 @@ describe("textWysiwyg", () => {
|
||||
editor.blur();
|
||||
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBe(156);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||
|
||||
mouse.select(rectangle);
|
||||
@ -1200,9 +1200,12 @@ describe("textWysiwyg", () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.height).toBe(156);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
// cache updated again
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
||||
155,
|
||||
8,
|
||||
);
|
||||
});
|
||||
|
||||
it("should reset the container height cache when font properties updated", async () => {
|
||||
|
@ -177,7 +177,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
it.skip("should add elements", async () => {
|
||||
h.elements = [rect2, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -188,7 +188,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
it.skip("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -199,7 +199,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
|
132
src/frame.ts
132
src/frame.ts
@ -14,7 +14,7 @@ import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
} from "./element/textElement";
|
||||
import { arrayToMap, findIndex } from "./utils";
|
||||
import { arrayToMap } from "./utils";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
@ -457,85 +457,87 @@ export const addElementsToFrame = (
|
||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||
frame: ExcalidrawFrameElement,
|
||||
) => {
|
||||
const _elementsToAdd: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elementsToAdd) {
|
||||
_elementsToAdd.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToAdd.push(boundTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
const allElementsIndex = allElements.reduce(
|
||||
(acc: Record<string, number>, element, index) => {
|
||||
acc[element.id] = index;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
const currTargetFrameChildrenMap = new Map(
|
||||
allElements.reduce(
|
||||
(acc: [ExcalidrawElement["id"], ExcalidrawElement][], element) => {
|
||||
if (element.frameId === frame.id) {
|
||||
acc.push([element.id, element]);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
const frameIndex = allElementsIndex[frame.id];
|
||||
// need to be calculated before the mutation below occurs
|
||||
const leftFrameBoundaryIndex = findIndex(
|
||||
allElements,
|
||||
(e) => e.frameId === frame.id,
|
||||
);
|
||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||
|
||||
const existingFrameChildren = allElements.filter(
|
||||
(element) => element.frameId === frame.id,
|
||||
);
|
||||
|
||||
const addedFrameChildren_left: ExcalidrawElement[] = [];
|
||||
const addedFrameChildren_right: ExcalidrawElement[] = [];
|
||||
const finalElementsToAdd: ExcalidrawElement[] = [];
|
||||
|
||||
// - add bound text elements if not already in the array
|
||||
// - filter out elements that are already in the frame
|
||||
for (const element of omitGroupsContainingFrames(
|
||||
allElements,
|
||||
_elementsToAdd,
|
||||
elementsToAdd,
|
||||
)) {
|
||||
if (element.frameId !== frame.id && !isFrameElement(element)) {
|
||||
if (allElementsIndex[element.id] > frameIndex) {
|
||||
addedFrameChildren_right.push(element);
|
||||
} else {
|
||||
addedFrameChildren_left.push(element);
|
||||
}
|
||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||
finalElementsToAdd.push(element);
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (
|
||||
boundTextElement &&
|
||||
!suppliedElementsToAddSet.has(boundTextElement.id) &&
|
||||
!currTargetFrameChildrenMap.has(boundTextElement.id)
|
||||
) {
|
||||
finalElementsToAdd.push(boundTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
const frameElement = allElements[frameIndex];
|
||||
const nextFrameChildren = addedFrameChildren_left
|
||||
.concat(existingFrameChildren)
|
||||
.concat(addedFrameChildren_right);
|
||||
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
|
||||
|
||||
const nextFrameChildrenMap = nextFrameChildren.reduce(
|
||||
(acc: Record<string, boolean>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const nextElements: ExcalidrawElement[] = [];
|
||||
|
||||
const nextOtherElements_left = allElements
|
||||
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
const processedElements = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
const nextOtherElement_right = allElements
|
||||
.slice(frameIndex + 1)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
for (const element of allElements) {
|
||||
if (processedElements.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextElements = nextOtherElements_left
|
||||
.concat(nextFrameChildren)
|
||||
.concat([frameElement])
|
||||
.concat(nextOtherElement_right);
|
||||
processedElements.add(element.id);
|
||||
|
||||
if (
|
||||
finalElementsToAddSet.has(element.id) ||
|
||||
(element.frameId && element.frameId === frame.id)
|
||||
) {
|
||||
// will be added in bulk once we process target frame
|
||||
continue;
|
||||
}
|
||||
|
||||
// target frame
|
||||
if (element.id === frame.id) {
|
||||
const currFrameChildren = getFrameElements(allElements, frame.id);
|
||||
currFrameChildren.forEach((child) => {
|
||||
processedElements.add(child.id);
|
||||
});
|
||||
// console.log(currFrameChildren, finalElementsToAdd, element);
|
||||
nextElements.push(...currFrameChildren, ...finalElementsToAdd, element);
|
||||
continue;
|
||||
}
|
||||
|
||||
// console.log("(2)", element.frameId);
|
||||
nextElements.push(element);
|
||||
}
|
||||
|
||||
for (const element of finalElementsToAdd) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return nextElements;
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ export const CODES = {
|
||||
V: "KeyV",
|
||||
Z: "KeyZ",
|
||||
R: "KeyR",
|
||||
S: "KeyS",
|
||||
} as const;
|
||||
|
||||
export const KEYS = {
|
||||
|
@ -164,6 +164,7 @@
|
||||
"darkMode": "Dark mode",
|
||||
"lightMode": "Light mode",
|
||||
"zenMode": "Zen mode",
|
||||
"objectsSnapMode": "Snap to objects",
|
||||
"exitZenMode": "Exit zen mode",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
@ -235,6 +236,7 @@
|
||||
"eraser": "Eraser",
|
||||
"frame": "Frame tool",
|
||||
"embeddable": "Web Embed",
|
||||
"laser": "Laser pointer",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools"
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { rotate } from "./math";
|
||||
import { rangeIntersection, rangesOverlap, rotate } from "./math";
|
||||
|
||||
describe("rotate", () => {
|
||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||
@ -13,3 +13,43 @@ describe("rotate", () => {
|
||||
expect(res2).toEqual([x1, x2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("range overlap", () => {
|
||||
it("should overlap when range a contains range b", () => {
|
||||
expect(rangesOverlap([1, 4], [2, 3])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [1, 3])).toBe(true);
|
||||
expect(rangesOverlap([1, 4], [2, 4])).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range b contains range a", () => {
|
||||
expect(rangesOverlap([2, 3], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([1, 3], [1, 4])).toBe(true);
|
||||
expect(rangesOverlap([2, 4], [1, 4])).toBe(true);
|
||||
});
|
||||
|
||||
it("should overlap when range a and b intersect", () => {
|
||||
expect(rangesOverlap([1, 4], [2, 5])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("range intersection", () => {
|
||||
it("should intersect completely with itself", () => {
|
||||
expect(rangeIntersection([1, 4], [1, 4])).toEqual([1, 4]);
|
||||
});
|
||||
|
||||
it("should intersect irrespective of order", () => {
|
||||
expect(rangeIntersection([1, 4], [2, 3])).toEqual([2, 3]);
|
||||
expect(rangeIntersection([2, 3], [1, 4])).toEqual([2, 3]);
|
||||
expect(rangeIntersection([1, 4], [3, 5])).toEqual([3, 4]);
|
||||
expect(rangeIntersection([3, 5], [1, 4])).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it("should intersect at the edge", () => {
|
||||
expect(rangeIntersection([1, 4], [4, 5])).toEqual([4, 4]);
|
||||
});
|
||||
|
||||
it("should not intersect", () => {
|
||||
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
33
src/math.ts
33
src/math.ts
@ -472,3 +472,36 @@ export const isRightAngle = (angle: number) => {
|
||||
// angle, which we can check with modulo after rounding.
|
||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
||||
};
|
||||
|
||||
// Given two ranges, return if the two ranges overlap with each other
|
||||
// e.g. [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5]
|
||||
export const rangesOverlap = (
|
||||
[a0, a1]: [number, number],
|
||||
[b0, b1]: [number, number],
|
||||
) => {
|
||||
if (a0 <= b0) {
|
||||
return a1 >= b0;
|
||||
}
|
||||
|
||||
if (a0 >= b0) {
|
||||
return b1 >= a0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Given two ranges,return ther intersection of the two ranges if any
|
||||
// e.g. the intersection of [1, 3] and [2, 4] is [2, 3]
|
||||
export const rangeIntersection = (
|
||||
rangeA: [number, number],
|
||||
rangeB: [number, number],
|
||||
): [number, number] | null => {
|
||||
const rangeStart = Math.max(rangeA[0], rangeB[0]);
|
||||
const rangeEnd = Math.min(rangeA[1], rangeB[1]);
|
||||
|
||||
if (rangeStart <= rangeEnd) {
|
||||
return [rangeStart, rangeEnd];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -11,6 +11,28 @@ The change should be grouped under one of the below section and must contain PR
|
||||
Please add the latest change on the top under the correct section.
|
||||
-->
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [7078](https://github.com/excalidraw/excalidraw/pull/7078)
|
||||
|
||||
## 0.16.1 (2023-09-21)
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Fixes
|
||||
|
||||
- More eye-droper fixes [#7019](https://github.com/excalidraw/excalidraw/pull/7019)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move excalidraw-app outside src [#6987](https://github.com/excalidraw/excalidraw/pull/6987)
|
||||
|
||||
---
|
||||
|
||||
## 0.16.0 (2023-09-19)
|
||||
|
||||
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||
|
@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
children,
|
||||
validateEmbeddable,
|
||||
renderEmbeddable,
|
||||
activeTool,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
@ -119,6 +120,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onScrollChange={onScrollChange}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
activeTool={activeTool}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@excalidraw/excalidraw",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"main": "main.js",
|
||||
"types": "types/packages/excalidraw/index.d.ts",
|
||||
"files": [
|
||||
|
@ -22,5 +22,12 @@ const polyfill = () => {
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Element.prototype.replaceChildren) {
|
||||
Element.prototype.replaceChildren = function (...nodes) {
|
||||
this.innerHTML = "";
|
||||
this.append(...nodes);
|
||||
};
|
||||
}
|
||||
};
|
||||
export default polyfill;
|
||||
|
@ -67,6 +67,7 @@ import {
|
||||
EXTERNAL_LINK_IMG,
|
||||
getLinkHandleFromCoords,
|
||||
} from "../element/Hyperlink";
|
||||
import { renderSnaps } from "./renderSnaps";
|
||||
import {
|
||||
isEmbeddableElement,
|
||||
isFrameElement,
|
||||
@ -720,6 +721,8 @@ const _renderInteractiveScene = ({
|
||||
context.restore();
|
||||
}
|
||||
|
||||
renderSnaps(context, appState);
|
||||
|
||||
// Reset zoom
|
||||
context.restore();
|
||||
|
||||
|
189
src/renderer/renderSnaps.ts
Normal file
189
src/renderer/renderSnaps.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||
import { InteractiveCanvasAppState, Point } from "../types";
|
||||
|
||||
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
||||
const SNAP_COLOR_DARK = "#ff0000";
|
||||
const SNAP_WIDTH = 1;
|
||||
const SNAP_CROSS_SIZE = 2;
|
||||
|
||||
export const renderSnaps = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
if (!appState.snapLines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in dark mode, we need to adjust the color to account for color inversion.
|
||||
// Don't change if zen mode, because we draw only crosses, we want the
|
||||
// colors to be more visible
|
||||
const snapColor =
|
||||
appState.theme === "light" || appState.zenModeEnabled
|
||||
? SNAP_COLOR_LIGHT
|
||||
: SNAP_COLOR_DARK;
|
||||
// in zen mode make the cross more visible since we don't draw the lines
|
||||
const snapWidth =
|
||||
(appState.zenModeEnabled ? SNAP_WIDTH * 1.5 : SNAP_WIDTH) /
|
||||
appState.zoom.value;
|
||||
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
|
||||
for (const snapLine of appState.snapLines) {
|
||||
if (snapLine.type === "pointer") {
|
||||
context.lineWidth = snapWidth;
|
||||
context.strokeStyle = snapColor;
|
||||
|
||||
drawPointerSnapLine(snapLine, context, appState);
|
||||
} else if (snapLine.type === "gap") {
|
||||
context.lineWidth = snapWidth;
|
||||
context.strokeStyle = snapColor;
|
||||
|
||||
drawGapLine(
|
||||
snapLine.points[0],
|
||||
snapLine.points[1],
|
||||
snapLine.direction,
|
||||
appState,
|
||||
context,
|
||||
);
|
||||
} else if (snapLine.type === "points") {
|
||||
context.lineWidth = snapWidth;
|
||||
context.strokeStyle = snapColor;
|
||||
drawPointsSnapLine(snapLine, context, appState);
|
||||
}
|
||||
}
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const drawPointsSnapLine = (
|
||||
pointSnapLine: PointSnapLine,
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
if (!appState.zenModeEnabled) {
|
||||
const firstPoint = pointSnapLine.points[0];
|
||||
const lastPoint = pointSnapLine.points[pointSnapLine.points.length - 1];
|
||||
|
||||
drawLine(firstPoint, lastPoint, context);
|
||||
}
|
||||
|
||||
for (const point of pointSnapLine.points) {
|
||||
drawCross(point, appState, context);
|
||||
}
|
||||
};
|
||||
|
||||
const drawPointerSnapLine = (
|
||||
pointerSnapLine: PointerSnapLine,
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
drawCross(pointerSnapLine.points[0], appState, context);
|
||||
if (!appState.zenModeEnabled) {
|
||||
drawLine(pointerSnapLine.points[0], pointerSnapLine.points[1], context);
|
||||
}
|
||||
};
|
||||
|
||||
const drawCross = (
|
||||
[x, y]: Point,
|
||||
appState: InteractiveCanvasAppState,
|
||||
context: CanvasRenderingContext2D,
|
||||
) => {
|
||||
context.save();
|
||||
const size =
|
||||
(appState.zenModeEnabled ? SNAP_CROSS_SIZE * 1.5 : SNAP_CROSS_SIZE) /
|
||||
appState.zoom.value;
|
||||
context.beginPath();
|
||||
|
||||
context.moveTo(x - size, y - size);
|
||||
context.lineTo(x + size, y + size);
|
||||
|
||||
context.moveTo(x + size, y - size);
|
||||
context.lineTo(x - size, y + size);
|
||||
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const drawLine = (
|
||||
from: Point,
|
||||
to: Point,
|
||||
context: CanvasRenderingContext2D,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.lineTo(...from);
|
||||
context.lineTo(...to);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const drawGapLine = (
|
||||
from: Point,
|
||||
to: Point,
|
||||
direction: "horizontal" | "vertical",
|
||||
appState: InteractiveCanvasAppState,
|
||||
context: CanvasRenderingContext2D,
|
||||
) => {
|
||||
// a horizontal gap snap line
|
||||
// |–––––––||–––––––|
|
||||
// ^ ^ ^ ^
|
||||
// \ \ \ \
|
||||
// (1) (2) (3) (4)
|
||||
|
||||
const FULL = 8 / appState.zoom.value;
|
||||
const HALF = FULL / 2;
|
||||
const QUARTER = FULL / 4;
|
||||
|
||||
if (direction === "horizontal") {
|
||||
const halfPoint = [(from[0] + to[0]) / 2, from[1]];
|
||||
// (1)
|
||||
if (!appState.zenModeEnabled) {
|
||||
drawLine([from[0], from[1] - FULL], [from[0], from[1] + FULL], context);
|
||||
}
|
||||
|
||||
// (3)
|
||||
drawLine(
|
||||
[halfPoint[0] - QUARTER, halfPoint[1] - HALF],
|
||||
[halfPoint[0] - QUARTER, halfPoint[1] + HALF],
|
||||
context,
|
||||
);
|
||||
drawLine(
|
||||
[halfPoint[0] + QUARTER, halfPoint[1] - HALF],
|
||||
[halfPoint[0] + QUARTER, halfPoint[1] + HALF],
|
||||
context,
|
||||
);
|
||||
|
||||
if (!appState.zenModeEnabled) {
|
||||
// (4)
|
||||
drawLine([to[0], to[1] - FULL], [to[0], to[1] + FULL], context);
|
||||
|
||||
// (2)
|
||||
drawLine(from, to, context);
|
||||
}
|
||||
} else {
|
||||
const halfPoint = [from[0], (from[1] + to[1]) / 2];
|
||||
// (1)
|
||||
if (!appState.zenModeEnabled) {
|
||||
drawLine([from[0] - FULL, from[1]], [from[0] + FULL, from[1]], context);
|
||||
}
|
||||
|
||||
// (3)
|
||||
drawLine(
|
||||
[halfPoint[0] - HALF, halfPoint[1] - QUARTER],
|
||||
[halfPoint[0] + HALF, halfPoint[1] - QUARTER],
|
||||
context,
|
||||
);
|
||||
drawLine(
|
||||
[halfPoint[0] - HALF, halfPoint[1] + QUARTER],
|
||||
[halfPoint[0] + HALF, halfPoint[1] + QUARTER],
|
||||
context,
|
||||
);
|
||||
|
||||
if (!appState.zenModeEnabled) {
|
||||
// (4)
|
||||
drawLine([to[0] - FULL, to[1]], [to[0] + FULL, to[1]], context);
|
||||
|
||||
// (2)
|
||||
drawLine(from, to, context);
|
||||
}
|
||||
}
|
||||
};
|
@ -11,6 +11,7 @@ import {
|
||||
getFrameElements,
|
||||
} from "../frame";
|
||||
import { isShallowEqual } from "../utils";
|
||||
import { isElementInViewport } from "../element/sizeHelpers";
|
||||
|
||||
/**
|
||||
* Frames and their containing elements are not to be selected at the same time.
|
||||
@ -89,6 +90,26 @@ export const getElementsWithinSelection = (
|
||||
return elementsInSelection;
|
||||
};
|
||||
|
||||
export const getVisibleAndNonSelectedElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const selectedElementsSet = new Set(
|
||||
selectedElements.map((element) => element.id),
|
||||
);
|
||||
return elements.filter((element) => {
|
||||
const isVisible = isElementInViewport(
|
||||
element,
|
||||
appState.width,
|
||||
appState.height,
|
||||
appState,
|
||||
);
|
||||
|
||||
return !selectedElementsSet.has(element.id) && isVisible;
|
||||
});
|
||||
};
|
||||
|
||||
// FIXME move this into the editor instance to keep utility methods stateless
|
||||
export const isSomeElementSelected = (function () {
|
||||
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
|
||||
|
1361
src/snapping.ts
Normal file
1361
src/snapping.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -331,12 +331,17 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -363,6 +368,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -524,12 +530,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -553,6 +561,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -666,7 +675,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
|
||||
exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `7`;
|
||||
exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `6`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -723,12 +732,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -752,6 +763,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -1039,7 +1051,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `13`;
|
||||
exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `12`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -1096,12 +1108,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -1125,6 +1139,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -1412,7 +1427,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `13`;
|
||||
exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `12`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -1469,12 +1484,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -1498,6 +1515,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -1611,7 +1629,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
|
||||
exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `7`;
|
||||
exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `6`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -1668,12 +1686,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -1695,6 +1715,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -1847,7 +1868,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
|
||||
exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `8`;
|
||||
exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `7`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -1904,12 +1925,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -1933,6 +1956,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -2148,7 +2172,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
|
||||
exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `8`;
|
||||
exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `7`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -2205,12 +2229,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -2239,6 +2265,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -2537,7 +2564,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
|
||||
exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `13`;
|
||||
exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `12`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -2594,12 +2621,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -2623,6 +2652,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -3416,7 +3446,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
|
||||
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `20`;
|
||||
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `19`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -3473,12 +3503,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -3502,6 +3534,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -3789,7 +3822,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
|
||||
exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `12`;
|
||||
exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `11`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -3846,12 +3879,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -3875,6 +3910,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -4162,7 +4198,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
|
||||
exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `12`;
|
||||
exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `11`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -4219,12 +4255,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -4251,6 +4289,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -4618,7 +4657,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
|
||||
exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `14`;
|
||||
exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `13`;
|
||||
|
||||
exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -4951,12 +4990,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -4983,6 +5024,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -5198,7 +5240,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
|
||||
exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `13`;
|
||||
exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `12`;
|
||||
|
||||
exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -5531,12 +5573,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -5565,6 +5609,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -5863,7 +5908,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
|
||||
exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`;
|
||||
|
||||
exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `14`;
|
||||
exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `13`;
|
||||
|
||||
exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = `
|
||||
{
|
||||
@ -5950,6 +5995,19 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
},
|
||||
"viewMode": true,
|
||||
},
|
||||
{
|
||||
"checked": [Function],
|
||||
"contextItemLabel": "buttons.objectsSnapMode",
|
||||
"keyTest": [Function],
|
||||
"name": "objectsSnapMode",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "canvas",
|
||||
"predicate": [Function],
|
||||
},
|
||||
"viewMode": true,
|
||||
},
|
||||
{
|
||||
"checked": [Function],
|
||||
"contextItemLabel": "buttons.zenMode",
|
||||
@ -6035,12 +6093,17 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -6062,6 +6125,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -6431,12 +6495,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": null,
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -6460,6 +6526,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -6805,12 +6872,17 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 20,
|
||||
"offsetTop": 10,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -6834,6 +6906,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": true,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
@ -7031,6 +7104,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] nu
|
||||
|
||||
exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`;
|
||||
|
||||
exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `7`;
|
||||
exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `6`;
|
||||
|
||||
exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `6`;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -87,6 +87,7 @@ describe("contextMenu element", () => {
|
||||
"gridMode",
|
||||
"zenMode",
|
||||
"viewMode",
|
||||
"objectsSnapMode",
|
||||
"stats",
|
||||
];
|
||||
|
||||
|
@ -47,7 +47,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -79,7 +79,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
@ -112,7 +112,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -144,7 +144,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -180,7 +180,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -221,7 +221,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -241,7 +241,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -261,7 +261,7 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -288,7 +288,7 @@ describe("Test dragCreate", () => {
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -315,7 +315,7 @@ describe("Test dragCreate", () => {
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
@ -1048,14 +1048,14 @@ describe("Test Linear Elements", () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"height": 130,
|
||||
"width": 367,
|
||||
"width": 366.11716195150507,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(getBoundTextElementPosition(container, textElement))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"x": 272,
|
||||
"x": 271.11716195150507,
|
||||
"y": 45,
|
||||
}
|
||||
`);
|
||||
@ -1069,9 +1069,9 @@ describe("Test Linear Elements", () => {
|
||||
[
|
||||
20,
|
||||
35,
|
||||
502,
|
||||
501.11716195150507,
|
||||
95,
|
||||
205.9061448421403,
|
||||
205.4589377083102,
|
||||
52.5,
|
||||
]
|
||||
`);
|
||||
|
@ -43,7 +43,7 @@ describe("move element", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -84,8 +84,8 @@ describe("move element", () => {
|
||||
// select the second rectangles
|
||||
new Pointer("mouse").clickOn(rectB);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(20);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(24);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(19);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
@ -131,7 +131,7 @@ describe("duplicate element on move when ALT is clicked", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
|
@ -48,7 +48,7 @@ describe("remove shape in non linear elements", () => {
|
||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
||||
@ -63,7 +63,7 @@ describe("remove shape in non linear elements", () => {
|
||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
||||
@ -78,7 +78,7 @@ describe("remove shape in non linear elements", () => {
|
||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@ -110,8 +110,8 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
@ -153,9 +153,8 @@ describe("multi point mode in linear elements", () => {
|
||||
fireEvent.keyDown(document, {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
@ -55,10 +55,15 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "name",
|
||||
"objectsSnapModeEnabled": false,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
"openPopup": null,
|
||||
"openSidebar": null,
|
||||
"originSnapOffset": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"pasteDialog": {
|
||||
"data": null,
|
||||
"shown": false,
|
||||
@ -80,6 +85,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"showWelcomeScreen": false,
|
||||
"snapLines": [],
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": [],
|
||||
"theme": "light",
|
||||
|
@ -310,7 +310,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -342,7 +342,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -374,7 +374,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -419,7 +419,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -463,7 +463,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
|
89
src/types.ts
89
src/types.ts
@ -18,7 +18,6 @@ import {
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
} from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||
import { SuggestedBinding } from "./element/binding";
|
||||
@ -34,15 +33,13 @@ import Library from "./data/library";
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||
import { ContextMenuItems } from "./components/ContextMenu";
|
||||
import { SnapLine } from "./snapping";
|
||||
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
||||
|
||||
export type Point = Readonly<RoughPoint>;
|
||||
|
||||
export type Collaborator = {
|
||||
pointer?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
pointer?: CollaboratorPointer;
|
||||
button?: "up" | "down";
|
||||
selectedElementIds?: AppState["selectedElementIds"];
|
||||
username?: string | null;
|
||||
@ -58,6 +55,12 @@ export type Collaborator = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type CollaboratorPointer = {
|
||||
x: number;
|
||||
y: number;
|
||||
tool: "pointer" | "laser";
|
||||
};
|
||||
|
||||
export type DataURL = string & { _brand: "DataURL" };
|
||||
|
||||
export type BinaryFileData = {
|
||||
@ -85,21 +88,31 @@ export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
|
||||
|
||||
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
||||
|
||||
export type LastActiveTool =
|
||||
export type ToolType =
|
||||
| "selection"
|
||||
| "rectangle"
|
||||
| "diamond"
|
||||
| "ellipse"
|
||||
| "arrow"
|
||||
| "line"
|
||||
| "freedraw"
|
||||
| "text"
|
||||
| "image"
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
| "embeddable"
|
||||
| "laser";
|
||||
|
||||
export type ActiveTool =
|
||||
| {
|
||||
type:
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
| "embeddable";
|
||||
type: ToolType;
|
||||
customType: null;
|
||||
}
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
}
|
||||
| null;
|
||||
};
|
||||
|
||||
export type SidebarName = string;
|
||||
export type SidebarTabName = string;
|
||||
@ -150,6 +163,9 @@ export type InteractiveCanvasAppState = Readonly<
|
||||
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
||||
// Collaborators
|
||||
collaborators: AppState["collaborators"];
|
||||
// SnapLines
|
||||
snapLines: AppState["snapLines"];
|
||||
zenModeEnabled: AppState["zenModeEnabled"];
|
||||
}
|
||||
>;
|
||||
|
||||
@ -191,23 +207,9 @@ export type AppState = {
|
||||
* indicates a previous tool we should revert back to if we deselect the
|
||||
* currently active tool. At the moment applies to `eraser` and `hand` tool.
|
||||
*/
|
||||
lastActiveTool: LastActiveTool;
|
||||
lastActiveTool: ActiveTool | null;
|
||||
locked: boolean;
|
||||
} & (
|
||||
| {
|
||||
type:
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
| "embeddable";
|
||||
customType: null;
|
||||
}
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
}
|
||||
);
|
||||
} & ActiveTool;
|
||||
penMode: boolean;
|
||||
penDetected: boolean;
|
||||
exportBackground: boolean;
|
||||
@ -287,6 +289,13 @@ export type AppState = {
|
||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||
showHyperlinkPopup: false | "info" | "editor";
|
||||
selectedLinearElement: LinearElementEditor | null;
|
||||
|
||||
snapLines: readonly SnapLine[];
|
||||
originSnapOffset: {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null;
|
||||
objectsSnapModeEnabled: boolean;
|
||||
};
|
||||
|
||||
export type UIAppState = Omit<
|
||||
@ -384,7 +393,7 @@ export interface ExcalidrawProps {
|
||||
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
||||
isCollaborating?: boolean;
|
||||
onPointerUpdate?: (payload: {
|
||||
pointer: { x: number; y: number };
|
||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => void;
|
||||
@ -400,6 +409,7 @@ export interface ExcalidrawProps {
|
||||
viewModeEnabled?: boolean;
|
||||
zenModeEnabled?: boolean;
|
||||
gridModeEnabled?: boolean;
|
||||
objectsSnapModeEnabled?: boolean;
|
||||
libraryReturnUrl?: string;
|
||||
theme?: Theme;
|
||||
name?: string;
|
||||
@ -435,6 +445,14 @@ export interface ExcalidrawProps {
|
||||
element: NonDeleted<ExcalidrawEmbeddableElement>,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
activeTool?:
|
||||
| {
|
||||
type: ToolType;
|
||||
}
|
||||
| {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
@ -527,6 +545,8 @@ export type AppClassProperties = {
|
||||
onInsertElements: App["onInsertElements"];
|
||||
onExportImage: App["onExportImage"];
|
||||
lastViewportPosition: App["lastViewportPosition"];
|
||||
togglePenMode: App["togglePenMode"];
|
||||
setActiveTool: App["setActiveTool"];
|
||||
};
|
||||
|
||||
export type PointerDownState = Readonly<{
|
||||
@ -653,3 +673,10 @@ export type FrameNameBoundsCache = {
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type KeyboardModifiersObject = {
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
altKey: boolean;
|
||||
metaKey: boolean;
|
||||
};
|
||||
|
12
src/utils.ts
12
src/utils.ts
@ -15,9 +15,8 @@ import {
|
||||
FontString,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
|
||||
import { ActiveTool, AppState, DataURL, ToolType, Zoom } from "./types";
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { isEraserActive, isHandToolActive } from "./appState";
|
||||
import { ResolutionType } from "./utility-types";
|
||||
import React from "react";
|
||||
@ -371,15 +370,10 @@ export const updateActiveTool = (
|
||||
appState: Pick<AppState, "activeTool">,
|
||||
data: (
|
||||
| {
|
||||
type:
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
| "embeddable";
|
||||
type: ToolType;
|
||||
}
|
||||
| { type: "custom"; customType: string }
|
||||
) & { lastActiveToolBeforeEraser?: LastActiveTool },
|
||||
) & { lastActiveToolBeforeEraser?: ActiveTool | null },
|
||||
): AppState["activeTool"] => {
|
||||
if (data.type === "custom") {
|
||||
return {
|
||||
|
@ -111,7 +111,7 @@ export default defineConfig({
|
||||
{
|
||||
src: "apple-touch-icon.png",
|
||||
type: "image/png",
|
||||
sizes: "256x256",
|
||||
sizes: "180x180",
|
||||
},
|
||||
],
|
||||
start_url: "/",
|
||||
|
@ -1522,6 +1522,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd"
|
||||
integrity sha512-GemHNF5Z6ga0BWBSX7GJaNBUchLu6RwTcAB84eX1MeckRNhNasAsPCdelDlFalz27iS4RuYEQh0bPE8SRxJgbQ==
|
||||
|
||||
"@excalidraw/laser-pointer@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.2.0.tgz#cd34ea7d24b11743c726488cc1fcb28c161cacba"
|
||||
integrity sha512-WjFFwLk9ahmKRKku7U0jqYpeM3fe9ZS1K43pfwPREHk4/FYU3iKDKVeS8m4tEAASnRlBt3hhLCBQLBF2uvgOnw==
|
||||
|
||||
"@excalidraw/prettier-config@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
||||
|
Reference in New Issue
Block a user