mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-06 13:54:24 +01:00
Compare commits
16 Commits
v0.16.3
...
zsviczian-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34daf09b4a | ||
|
|
2bca4c258d | ||
|
|
b0ca8f8126 | ||
|
|
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
|
## 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).
|
- 📡 PWA support (works offline).
|
||||||
- 🤼 Real-time collaboration.
|
- 🤼 Real-time collaboration.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ To render an item, its recommended to use `MainMenu.Item`.
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
| `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 |
|
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | - | The icon used in 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 |
|
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
|
||||||
@@ -70,6 +71,7 @@ function App() {
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
| `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. |
|
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
|
||||||
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Customizing Styles
|
# 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:
|
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`.
|
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
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
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. Run `yarn` to install dependencies
|
||||||
1. Create a branch for your PR with `git checkout -b your-branch-name`
|
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
|
> ```bash
|
||||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
> 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,
|
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||||
description: (
|
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?
|
start?
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
|
|||||||
type: "MOUSE_LOCATION";
|
type: "MOUSE_LOCATION";
|
||||||
payload: {
|
payload: {
|
||||||
socketId: string;
|
socketId: string;
|
||||||
pointer: { x: number; y: number };
|
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
|
"@excalidraw/laser-pointer": "1.2.0",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"src": "apple-touch-icon.png",
|
"src": "apple-touch-icon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "256x256"
|
"sizes": "180x180"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInvisiblySmallElement(multiPointElement)) {
|
if (isInvisiblySmallElement(multiPointElement)) {
|
||||||
newElements = newElements.slice(0, -1);
|
newElements = newElements.filter(
|
||||||
|
(el) => el.id !== multiPointElement.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const actionToggleGridMode = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||||
|
objectsSnapModeEnabled: false,
|
||||||
},
|
},
|
||||||
commitToHistory: 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 { actionToggleGridMode } from "./actionToggleGridMode";
|
||||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||||
|
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
|
||||||
|
|
||||||
export { actionToggleStats } from "./actionToggleStats";
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type ShortcutName =
|
|||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
@@ -74,6 +75,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
|
objectsSnapMode: [getShortcutKey("Alt+S")],
|
||||||
stats: [getShortcutKey("Alt+/")],
|
stats: [getShortcutKey("Alt+/")],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type ActionName =
|
|||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export const getDefaultAppState = (): Omit<
|
|||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
selectedLinearElement: null,
|
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 },
|
pendingImageElementId: { browser: false, export: false, server: false },
|
||||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||||
selectedLinearElement: { browser: true, 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 = <
|
const _clearAppStateForStorage = <
|
||||||
|
|||||||
@@ -14,13 +14,8 @@ import {
|
|||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { UIAppState, Zoom } from "../types";
|
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||||
import {
|
import { capitalizeString, isTransparent } from "../utils";
|
||||||
capitalizeString,
|
|
||||||
isTransparent,
|
|
||||||
updateActiveTool,
|
|
||||||
setCursorForShape,
|
|
||||||
} from "../utils";
|
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
@@ -36,7 +31,12 @@ import {
|
|||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
import {
|
||||||
|
EmbedIcon,
|
||||||
|
extraToolsIcon,
|
||||||
|
frameToolIcon,
|
||||||
|
laserPointerToolIcon,
|
||||||
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
@@ -215,18 +215,23 @@ export const SelectedShapeActions = ({
|
|||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
interactiveCanvas,
|
interactiveCanvas,
|
||||||
activeTool,
|
activeTool,
|
||||||
setAppState,
|
|
||||||
onImageAction,
|
onImageAction,
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
}: {
|
}: {
|
||||||
interactiveCanvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
activeTool: UIAppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
|
app: AppClassProperties;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
|
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
@@ -251,29 +256,14 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-${value}`}
|
data-testid={`toolbar-${value}`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
if (appState.activeTool.type !== value) {
|
if (appState.activeTool.type !== value) {
|
||||||
trackEvent("toolbar", value, "ui");
|
trackEvent("toolbar", value, "ui");
|
||||||
}
|
}
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: value });
|
||||||
type: value,
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
activeEmbeddable: null,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
});
|
|
||||||
setCursorForShape(interactiveCanvas, {
|
|
||||||
...appState,
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
});
|
|
||||||
if (value === "image") {
|
if (value === "image") {
|
||||||
onImageAction({ pointerType });
|
onImageAction({ pointerType });
|
||||||
}
|
}
|
||||||
@@ -300,24 +290,14 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-frame`}
|
data-testid={`toolbar-frame`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
trackEvent("toolbar", "frame", "ui");
|
trackEvent("toolbar", "frame", "ui");
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "frame" });
|
||||||
type: "frame",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
|
selected={activeTool.type === "frame"}
|
||||||
/>
|
/>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable: false })}
|
className={clsx("Shape", { fillable: false })}
|
||||||
@@ -330,30 +310,28 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-embeddable`}
|
data-testid={`toolbar-embeddable`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
trackEvent("toolbar", "embeddable", "ui");
|
trackEvent("toolbar", "embeddable", "ui");
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "embeddable" });
|
||||||
type: "embeddable",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
|
selected={activeTool.type === "embeddable"}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<DropdownMenu.Trigger
|
<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)}
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
title={t("toolBar.extraTools")}
|
title={t("toolBar.extraTools")}
|
||||||
>
|
>
|
||||||
@@ -366,37 +344,36 @@ export const ShapesSwitcher = ({
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "frame" });
|
||||||
type: "frame",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
icon={frameToolIcon}
|
icon={frameToolIcon}
|
||||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
data-testid="toolbar-frame"
|
data-testid="toolbar-frame"
|
||||||
|
selected={frameToolSelected}
|
||||||
>
|
>
|
||||||
{t("toolBar.frame")}
|
{t("toolBar.frame")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "embeddable" });
|
||||||
type: "embeddable",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
icon={EmbedIcon}
|
icon={EmbedIcon}
|
||||||
data-testid="toolbar-embeddable"
|
data-testid="toolbar-embeddable"
|
||||||
|
selected={embeddableToolSelected}
|
||||||
>
|
>
|
||||||
{t("toolBar.embeddable")}
|
{t("toolBar.embeddable")}
|
||||||
</DropdownMenu.Item>
|
</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.Content>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
actionLink,
|
actionLink,
|
||||||
actionToggleElementLock,
|
actionToggleElementLock,
|
||||||
actionToggleLinearEditor,
|
actionToggleLinearEditor,
|
||||||
|
actionToggleObjectsSnapMode,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
@@ -210,7 +211,7 @@ import {
|
|||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
|
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { findShapeByKey, SHAPES } from "../shapes";
|
import { findShapeByKey } from "../shapes";
|
||||||
import {
|
import {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
@@ -228,6 +229,9 @@ import {
|
|||||||
FrameNameBoundsCache,
|
FrameNameBoundsCache,
|
||||||
SidebarName,
|
SidebarName,
|
||||||
SidebarTabName,
|
SidebarTabName,
|
||||||
|
KeyboardModifiersObject,
|
||||||
|
CollaboratorPointer,
|
||||||
|
ToolType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@@ -342,6 +346,17 @@ import {
|
|||||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
|
import {
|
||||||
|
getSnapLinesAtPointer,
|
||||||
|
snapDraggedElements,
|
||||||
|
isActiveToolNonLinearSnappable,
|
||||||
|
snapNewElement,
|
||||||
|
snapResizingElements,
|
||||||
|
isSnappingEnabled,
|
||||||
|
getVisibleGaps,
|
||||||
|
getReferenceSnapPoints,
|
||||||
|
SnapCache,
|
||||||
|
} from "../snapping";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
@@ -354,6 +369,8 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
|||||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
||||||
|
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(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>();
|
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement>();
|
||||||
|
|
||||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||||
lastPointerDown: React.PointerEvent<HTMLElement> | null = null;
|
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
||||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
||||||
|
null;
|
||||||
lastViewportPosition = { x: 0, y: 0 };
|
lastViewportPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
laserPathManager: LaserPathManager = new LaserPathManager(this);
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
@@ -490,6 +510,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
zenModeEnabled = false,
|
zenModeEnabled = false,
|
||||||
gridModeEnabled = false,
|
gridModeEnabled = false,
|
||||||
|
objectsSnapModeEnabled = false,
|
||||||
theme = defaultAppState.theme,
|
theme = defaultAppState.theme,
|
||||||
name = defaultAppState.name,
|
name = defaultAppState.name,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -500,6 +521,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
...this.getCanvasOffsets(),
|
...this.getCanvasOffsets(),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
objectsSnapModeEnabled,
|
||||||
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||||
name,
|
name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
@@ -1086,7 +1108,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
cursor: CURSOR_TYPE.MOVE,
|
cursor: CURSOR_TYPE.MOVE,
|
||||||
pointerEvents: this.state.viewModeEnabled
|
pointerEvents: this.state.viewModeEnabled
|
||||||
? POINTER_EVENTS.disabled
|
? POINTER_EVENTS.disabled
|
||||||
: POINTER_EVENTS.inheritFromUI,
|
: POINTER_EVENTS.enabled,
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => this.handleCanvasPointerDown(event)}
|
onPointerDown={(event) => this.handleCanvasPointerDown(event)}
|
||||||
onWheel={(event) => this.handleWheel(event)}
|
onWheel={(event) => this.handleWheel(event)}
|
||||||
@@ -1188,12 +1210,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
}
|
}
|
||||||
app={this}
|
app={this}
|
||||||
|
isCollaborating={this.props.isCollaborating}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
|
<LaserToolOverlay manager={this.laserPathManager} />
|
||||||
{selectedElements.length === 1 &&
|
{selectedElements.length === 1 &&
|
||||||
!this.state.contextMenu &&
|
!this.state.contextMenu &&
|
||||||
this.state.showHyperlinkPopup && (
|
this.state.showHyperlinkPopup && (
|
||||||
@@ -1721,7 +1745,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
this.scene.destroy();
|
this.scene.destroy();
|
||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
|
this.laserPathManager.destroy();
|
||||||
ShapeCache.destroy();
|
ShapeCache.destroy();
|
||||||
|
SnapCache.destroy();
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
isSomeElementSelected.clearCache();
|
isSomeElementSelected.clearCache();
|
||||||
selectGroupsForSelectedElements.clearCache();
|
selectGroupsForSelectedElements.clearCache();
|
||||||
@@ -2536,10 +2562,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
togglePenMode = () => {
|
togglePenMode = (force?: boolean) => {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
return {
|
return {
|
||||||
penMode: !prevState.penMode,
|
penMode: force ?? !prevState.penMode,
|
||||||
|
penDetected: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3033,6 +3060,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 (
|
if (
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||||
@@ -3092,15 +3128,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
private setActiveTool = (
|
setActiveTool = (
|
||||||
tool:
|
tool:
|
||||||
| {
|
| {
|
||||||
type:
|
type: ToolType;
|
||||||
| typeof SHAPES[number]["value"]
|
|
||||||
| "eraser"
|
|
||||||
| "hand"
|
|
||||||
| "frame"
|
|
||||||
| "embeddable";
|
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string },
|
| { type: "custom"; customType: string },
|
||||||
) => {
|
) => {
|
||||||
@@ -3119,17 +3150,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (nextActiveTool.type === "image") {
|
if (nextActiveTool.type === "image") {
|
||||||
this.onImageAction();
|
this.onImageAction();
|
||||||
}
|
}
|
||||||
if (nextActiveTool.type !== "selection") {
|
|
||||||
this.setState({
|
this.setState((prevState) => {
|
||||||
activeTool: nextActiveTool,
|
const commonResets = {
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
snapLines: prevState.snapLines.length ? [] : prevState.snapLines,
|
||||||
selectedGroupIds: {},
|
originSnapOffset: null,
|
||||||
editingGroupId: null,
|
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
});
|
} as const;
|
||||||
} else {
|
if (nextActiveTool.type !== "selection") {
|
||||||
this.setState({ activeTool: nextActiveTool, activeEmbeddable: null });
|
return {
|
||||||
}
|
...prevState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||||
|
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
|
||||||
|
editingGroupId: null,
|
||||||
|
multiElement: null,
|
||||||
|
...commonResets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
...commonResets,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private setCursor = (cursor: string) => {
|
private setCursor = (cursor: string) => {
|
||||||
@@ -3706,10 +3750,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isTouchScreen: boolean,
|
isTouchScreen: boolean,
|
||||||
) => {
|
) => {
|
||||||
const draggedDistance = distance2d(
|
const draggedDistance = distance2d(
|
||||||
this.lastPointerDown!.clientX,
|
this.lastPointerDownEvent!.clientX,
|
||||||
this.lastPointerDown!.clientY,
|
this.lastPointerDownEvent!.clientY,
|
||||||
this.lastPointerUp!.clientX,
|
this.lastPointerUpEvent!.clientX,
|
||||||
this.lastPointerUp!.clientY,
|
this.lastPointerUpEvent!.clientY,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!this.hitLinkElement ||
|
!this.hitLinkElement ||
|
||||||
@@ -3720,7 +3764,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
||||||
this.lastPointerDown!,
|
this.lastPointerDownEvent!,
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const lastPointerDownHittingLinkIcon = isPointHittingLink(
|
const lastPointerDownHittingLinkIcon = isPointHittingLink(
|
||||||
@@ -3730,7 +3774,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.device.isMobile,
|
this.device.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
this.lastPointerUp!,
|
this.lastPointerUpEvent!,
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const lastPointerUpHittingLinkIcon = isPointHittingLink(
|
const lastPointerUpHittingLinkIcon = isPointHittingLink(
|
||||||
@@ -3865,6 +3909,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
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 (
|
if (
|
||||||
this.state.editingLinearElement &&
|
this.state.editingLinearElement &&
|
||||||
!this.state.editingLinearElement.isDragging
|
!this.state.editingLinearElement.isDragging
|
||||||
@@ -4335,6 +4403,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ contextMenu: null });
|
this.setState({ contextMenu: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.snapLines) {
|
||||||
|
this.setAppState({ snapLines: [] });
|
||||||
|
}
|
||||||
|
|
||||||
this.updateGestureOnPointerDown(event);
|
this.updateGestureOnPointerDown(event);
|
||||||
|
|
||||||
// if dragging element is freedraw and another pointerdown event occurs
|
// if dragging element is freedraw and another pointerdown event occurs
|
||||||
@@ -4407,17 +4479,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastPointerDown = event;
|
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPointerDownEvent = event;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
lastPointerDownWith: event.pointerType,
|
lastPointerDownWith: event.pointerType,
|
||||||
cursorButton: "down",
|
cursorButton: "down",
|
||||||
});
|
});
|
||||||
this.savePointer(event.clientX, event.clientY, "down");
|
this.savePointer(event.clientX, event.clientY, "down");
|
||||||
|
|
||||||
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only handle left mouse button or touch
|
// only handle left mouse button or touch
|
||||||
if (
|
if (
|
||||||
event.button !== POINTER_BUTTON.MAIN &&
|
event.button !== POINTER_BUTTON.MAIN &&
|
||||||
@@ -4508,6 +4581,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (this.state.activeTool.type === "frame") {
|
} else if (this.state.activeTool.type === "frame") {
|
||||||
this.createFrameElementOnPointerDown(pointerDownState);
|
this.createFrameElementOnPointerDown(pointerDownState);
|
||||||
|
} else if (this.state.activeTool.type === "laser") {
|
||||||
|
this.laserPathManager.startPath(
|
||||||
|
pointerDownState.lastCoords.x,
|
||||||
|
pointerDownState.lastCoords.y,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
this.state.activeTool.type !== "eraser" &&
|
this.state.activeTool.type !== "eraser" &&
|
||||||
this.state.activeTool.type !== "hand"
|
this.state.activeTool.type !== "hand"
|
||||||
@@ -4531,7 +4609,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
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_MOVE, onPointerMove);
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||||
@@ -4547,14 +4625,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
this.removePointer(event);
|
this.removePointer(event);
|
||||||
this.lastPointerUp = event;
|
this.lastPointerUpEvent = event;
|
||||||
|
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{ clientX: event.clientX, clientY: event.clientY },
|
{ clientX: event.clientX, clientY: event.clientY },
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const clicklength =
|
const clicklength =
|
||||||
event.timeStamp - (this.lastPointerDown?.timeStamp ?? 0);
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||||
if (this.device.isMobile && clicklength < 300) {
|
if (this.device.isMobile && clicklength < 300) {
|
||||||
const hitElement = this.getElementAtPosition(
|
const hitElement = this.getElementAtPosition(
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
@@ -5308,7 +5386,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
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);
|
const embedLink = getEmbedLink(link);
|
||||||
@@ -5358,7 +5438,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
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({
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||||
@@ -5535,7 +5617,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
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({
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||||
@@ -5593,7 +5677,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
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({
|
const frame = newFrameElement({
|
||||||
@@ -5616,6 +5702,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(
|
private onKeyDownFromPointerDownHandler(
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): (event: KeyboardEvent) => void {
|
): (event: KeyboardEvent) => void {
|
||||||
@@ -5673,6 +5805,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.activeTool.type === "laser") {
|
||||||
|
this.laserPathManager.addPointToPath(pointerCoords.x, pointerCoords.y);
|
||||||
|
}
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
@@ -5845,33 +5981,62 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.editingElement &&
|
!this.state.editingElement &&
|
||||||
this.state.activeEmbeddable?.state !== "active"
|
this.state.activeEmbeddable?.state !== "active"
|
||||||
) {
|
) {
|
||||||
const [dragX, dragY] = getGridPoint(
|
const dragOffset = {
|
||||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
x: pointerCoords.x - pointerDownState.origin.x,
|
||||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
y: pointerCoords.y - pointerDownState.origin.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const [dragDistanceX, dragDistanceY] = [
|
const originalElements = [
|
||||||
Math.abs(pointerCoords.x - pointerDownState.origin.x),
|
...pointerDownState.originalElements.values(),
|
||||||
Math.abs(pointerCoords.y - pointerDownState.origin.y),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// We only drag in one direction if shift is pressed
|
// We only drag in one direction if shift is pressed
|
||||||
const lockDirection = event.shiftKey;
|
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
|
// when we're editing the name of a frame, we want the user to be
|
||||||
// able to select and interact with the text input
|
// able to select and interact with the text input
|
||||||
!this.state.editingFrame &&
|
!this.state.editingFrame &&
|
||||||
dragSelectedElements(
|
dragSelectedElements(
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
dragX,
|
dragOffset,
|
||||||
dragY,
|
|
||||||
lockDirection,
|
|
||||||
dragDistanceX,
|
|
||||||
dragDistanceY,
|
|
||||||
this.state,
|
this.state,
|
||||||
this.scene,
|
this.scene,
|
||||||
|
snapOffset,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
|
|
||||||
// We duplicate the selected element if alt is pressed on pointer move
|
// We duplicate the selected element if alt is pressed on pointer move
|
||||||
@@ -5912,15 +6077,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
const [originDragX, originDragY] = getGridPoint(
|
const origElement = pointerDownState.originalElements.get(
|
||||||
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
element.id,
|
||||||
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
)!;
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
|
||||||
);
|
|
||||||
mutateElement(duplicatedElement, {
|
mutateElement(duplicatedElement, {
|
||||||
x: duplicatedElement.x + (originDragX - dragX),
|
x: origElement.x,
|
||||||
y: duplicatedElement.y + (originDragY - dragY),
|
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);
|
nextElements.push(duplicatedElement);
|
||||||
elementsToAppend.push(element);
|
elementsToAppend.push(element);
|
||||||
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
||||||
@@ -5946,6 +6117,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
);
|
);
|
||||||
this.scene.replaceAllElements(nextSceneElements);
|
this.scene.replaceAllElements(nextSceneElements);
|
||||||
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6162,6 +6335,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isResizing,
|
isResizing,
|
||||||
isRotating,
|
isRotating,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
@@ -6176,8 +6350,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
multiElement || isTextElement(this.state.editingElement)
|
multiElement || isTextElement(this.state.editingElement)
|
||||||
? this.state.editingElement
|
? this.state.editingElement
|
||||||
: null,
|
: null,
|
||||||
|
snapLines: [],
|
||||||
|
|
||||||
|
originSnapOffset: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints(null);
|
||||||
|
SnapCache.setVisibleGaps(null);
|
||||||
|
|
||||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -6401,7 +6581,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
// remove invisible element which was added in onPointerDown
|
// remove invisible element which was added in onPointerDown
|
||||||
this.scene.replaceAllElements(
|
this.scene.replaceAllElements(
|
||||||
this.scene.getElementsIncludingDeleted().slice(0, -1),
|
this.scene
|
||||||
|
.getElementsIncludingDeleted()
|
||||||
|
.filter((el) => el.id !== draggingElement.id),
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
@@ -6623,17 +6805,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
if (isEraserActive(this.state)) {
|
if (isEraserActive(this.state)) {
|
||||||
const draggedDistance = distance2d(
|
const draggedDistance = distance2d(
|
||||||
this.lastPointerDown!.clientX,
|
this.lastPointerDownEvent!.clientX,
|
||||||
this.lastPointerDown!.clientY,
|
this.lastPointerDownEvent!.clientY,
|
||||||
this.lastPointerUp!.clientX,
|
this.lastPointerUpEvent!.clientX,
|
||||||
this.lastPointerUp!.clientY,
|
this.lastPointerUpEvent!.clientY,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (draggedDistance === 0) {
|
if (draggedDistance === 0) {
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{
|
{
|
||||||
clientX: this.lastPointerUp!.clientX,
|
clientX: this.lastPointerUpEvent!.clientX,
|
||||||
clientY: this.lastPointerUp!.clientY,
|
clientY: this.lastPointerUpEvent!.clientY,
|
||||||
},
|
},
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
@@ -6873,6 +7055,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
|
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeTool.type === "laser") {
|
||||||
|
this.laserPathManager.endPath();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -6889,14 +7076,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
this.lastPointerUp &&
|
this.lastPointerUpEvent &&
|
||||||
this.lastPointerDown &&
|
this.lastPointerDownEvent &&
|
||||||
this.lastPointerUp.timeStamp - this.lastPointerDown.timeStamp < 300 &&
|
this.lastPointerUpEvent.timeStamp -
|
||||||
|
this.lastPointerDownEvent.timeStamp <
|
||||||
|
300 &&
|
||||||
gesture.pointers.size <= 1 &&
|
gesture.pointers.size <= 1 &&
|
||||||
isEmbeddableElement(hitElement) &&
|
isEmbeddableElement(hitElement) &&
|
||||||
this.isEmbeddableCenter(
|
this.isEmbeddableCenter(
|
||||||
hitElement,
|
hitElement,
|
||||||
this.lastPointerUp,
|
this.lastPointerUpEvent,
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
)
|
)
|
||||||
@@ -7705,7 +7894,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [gridX, gridY] = getGridPoint(
|
let [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
@@ -7719,6 +7908,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? image.width / image.height
|
? image.width / image.height
|
||||||
: null;
|
: 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(
|
dragNewElement(
|
||||||
draggingElement,
|
draggingElement,
|
||||||
this.state.activeTool.type,
|
this.state.activeTool.type,
|
||||||
@@ -7733,6 +7949,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
|
this.state.originSnapOffset,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.maybeSuggestBindingForAll([draggingElement]);
|
this.maybeSuggestBindingForAll([draggingElement]);
|
||||||
@@ -7774,7 +7991,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
});
|
});
|
||||||
const pointerCoords = pointerDownState.lastCoords;
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
const [resizeX, resizeY] = getGridPoint(
|
let [resizeX, resizeY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
@@ -7802,6 +8019,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 (
|
if (
|
||||||
transformElements(
|
transformElements(
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
@@ -7817,6 +8069,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
resizeY,
|
resizeY,
|
||||||
pointerDownState.resize.center.x,
|
pointerDownState.resize.center.x,
|
||||||
pointerDownState.resize.center.y,
|
pointerDownState.resize.center.y,
|
||||||
|
this.state,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
@@ -7904,6 +8157,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionUnlockAllElements,
|
actionUnlockAllElements,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionToggleGridMode,
|
actionToggleGridMode,
|
||||||
|
actionToggleObjectsSnapMode,
|
||||||
actionToggleZenMode,
|
actionToggleZenMode,
|
||||||
actionToggleViewMode,
|
actionToggleViewMode,
|
||||||
actionToggleStats,
|
actionToggleStats,
|
||||||
@@ -8050,15 +8304,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!x || !y) {
|
if (!x || !y) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pointer = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
{ clientX: x, clientY: y },
|
{ clientX: x, clientY: y },
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNaN(pointer.x) || isNaN(pointer.y)) {
|
if (isNaN(sceneX) || isNaN(sceneY)) {
|
||||||
// sometimes the pointer goes off screen
|
// sometimes the pointer goes off screen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pointer: CollaboratorPointer = {
|
||||||
|
x: sceneX,
|
||||||
|
y: sceneY,
|
||||||
|
tool: this.state.activeTool.type === "laser" ? "laser" : "pointer",
|
||||||
|
};
|
||||||
|
|
||||||
this.props.onPointerUpdate?.({
|
this.props.onPointerUpdate?.({
|
||||||
pointer,
|
pointer,
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
||||||
|
<Shortcut label={t("toolBar.laser")} shortcuts={[KEYS.K]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.eyeDropper")}
|
label={t("labels.eyeDropper")}
|
||||||
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
||||||
@@ -258,6 +259,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("buttons.zenMode")}
|
label={t("buttons.zenMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("buttons.objectsSnapMode")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+S")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.showGrid")}
|
label={t("labels.showGrid")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||||
|
|||||||
309
src/components/LaserTool/LaserPathManager.ts
Normal file
309
src/components/LaserTool/LaserPathManager.ts
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
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 isDrawing = false;
|
||||||
|
private container: SVGSVGElement | undefined;
|
||||||
|
|
||||||
|
constructor(private app: App) {
|
||||||
|
this.ownState = instantiateCollabolatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stop();
|
||||||
|
this.isDrawing = false;
|
||||||
|
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.isDrawing = true;
|
||||||
|
|
||||||
|
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 (this.isDrawing) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let somePathsExist = false;
|
||||||
|
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paths.trim()) {
|
||||||
|
somePathsExist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
paths = paths.trim();
|
||||||
|
|
||||||
|
if (paths) {
|
||||||
|
somePathsExist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ownState.svg.setAttribute("d", paths);
|
||||||
|
this.ownState.svg.setAttribute("fill", "red");
|
||||||
|
|
||||||
|
if (!somePathsExist) {
|
||||||
|
this.isDrawing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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 { mutateElement } from "../element/mutateElement";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
|
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -77,6 +78,7 @@ interface LayerUIProps {
|
|||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
|
isCollaborating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@@ -134,6 +136,7 @@ const LayerUI = ({
|
|||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
app,
|
app,
|
||||||
|
isCollaborating,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -279,7 +282,7 @@ const LayerUI = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
interactiveCanvas={interactiveCanvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
app={app}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
onImageAction({
|
onImageAction({
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
@@ -288,6 +291,24 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</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.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const MobileMenu = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
interactiveCanvas={interactiveCanvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
app={app}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
onImageAction({
|
onImageAction({
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
|||||||
@@ -170,5 +170,10 @@
|
|||||||
height: var(--lg-icon-size);
|
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
|
box-shadow: 0 0 0 1px
|
||||||
var(--button-active-border, var(--color-primary-darkest)) inset;
|
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 {
|
.App-toolbar__extra-tools-dropdown {
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ const getRelevantAppStateProps = (
|
|||||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||||
activeEmbeddable: appState.activeEmbeddable,
|
activeEmbeddable: appState.activeEmbeddable,
|
||||||
|
snapLines: appState.snapLines,
|
||||||
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|||||||
@@ -59,6 +59,11 @@
|
|||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
--icon-fill-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ const DropdownMenuItem = ({
|
|||||||
children,
|
children,
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
|
selected,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
onSelect: (event: Event) => void;
|
onSelect: (event: Event) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
selected?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
@@ -26,7 +28,7 @@ const DropdownMenuItem = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={getDropdownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import React from "react";
|
|||||||
const DropdownMenuItemCustom = ({
|
const DropdownMenuItemCustom = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
|
selected,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
selected?: boolean;
|
||||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const DropdownMenuItemLink = ({
|
|||||||
children,
|
children,
|
||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
|
selected,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -19,6 +20,7 @@ const DropdownMenuItemLink = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
selected?: boolean;
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
@@ -29,7 +31,7 @@ const DropdownMenuItemLink = ({
|
|||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={getDropdownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
|||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
export const getDropdownMenuItemClassName = (className = "") => {
|
export const getDropdownMenuItemClassName = (
|
||||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
className = "",
|
||||||
|
selected = false,
|
||||||
|
) => {
|
||||||
|
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
||||||
|
selected ? "dropdown-menu-item--selected" : ""
|
||||||
|
}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleDropdownMenuItemClick = (
|
export const useHandleDropdownMenuItemClick = (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import clsx from "clsx";
|
|||||||
import { Theme } from "../element/types";
|
import { Theme } from "../element/types";
|
||||||
import { THEME } from "../constants";
|
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) =>
|
const handlerColor = (theme: Theme) =>
|
||||||
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||||
@@ -1653,3 +1653,22 @@ export const frameToolIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
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,
|
frame: true,
|
||||||
embeddable: true,
|
embeddable: true,
|
||||||
hand: true,
|
hand: true,
|
||||||
|
laser: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const getElementAbsoluteCoords = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* for a given element, `getElementLineSegments` returns line segments
|
* for a given element, `getElementLineSegments` returns line segments
|
||||||
* that can be used for visual collision detection (useful for frames)
|
* that can be used for visual collision detection (useful for frames)
|
||||||
* as opposed to bounding box collision detection
|
* as opposed to bounding box collision detection
|
||||||
@@ -674,6 +674,19 @@ export const getCommonBounds = (
|
|||||||
return [minX, minY, maxX, maxY];
|
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 = (
|
export const getResizedElementAbsoluteCoords = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { Bounds, getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
|
import { getGridPoint } from "../math";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isFrameElement } from "./typeChecks";
|
import { isFrameElement } from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
pointerX: number,
|
offset: { x: number; y: number },
|
||||||
pointerY: number,
|
|
||||||
lockDirection: boolean = false,
|
|
||||||
distanceX: number = 0,
|
|
||||||
distanceY: number = 0,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
scene: Scene,
|
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
|
// 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
|
// but when it happens (due to some bug), we want to avoid updating element
|
||||||
// in the frame twice, hence the use of set
|
// in the frame twice, hence the use of set
|
||||||
@@ -42,15 +41,16 @@ export const dragSelectedElements = (
|
|||||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commonBounds = getCommonBounds(Array.from(elementsToUpdate));
|
||||||
|
const adjustedOffset = calculateOffset(
|
||||||
|
commonBounds,
|
||||||
|
offset,
|
||||||
|
snapOffset,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(
|
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||||
lockDirection,
|
|
||||||
distanceX,
|
|
||||||
distanceY,
|
|
||||||
pointerDownState,
|
|
||||||
element,
|
|
||||||
offset,
|
|
||||||
);
|
|
||||||
// update coords of bound text only if we're dragging the container directly
|
// update coords of bound text only if we're dragging the container directly
|
||||||
// (we don't drag the group that it's part of)
|
// (we don't drag the group that it's part of)
|
||||||
if (
|
if (
|
||||||
@@ -68,14 +68,7 @@ export const dragSelectedElements = (
|
|||||||
// updating its coords again
|
// updating its coords again
|
||||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
(!textElement.frameId || !frames.includes(textElement.frameId))
|
||||||
) {
|
) {
|
||||||
updateElementCoords(
|
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||||
lockDirection,
|
|
||||||
distanceX,
|
|
||||||
distanceY,
|
|
||||||
pointerDownState,
|
|
||||||
textElement,
|
|
||||||
offset,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
@@ -84,32 +77,54 @@ export const dragSelectedElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateOffset = (
|
||||||
|
commonBounds: Bounds,
|
||||||
|
dragOffset: { x: number; y: number },
|
||||||
|
snapOffset: { x: number; y: number },
|
||||||
|
gridSize: AppState["gridSize"],
|
||||||
|
): { x: number; y: number } => {
|
||||||
|
const [x, y] = commonBounds;
|
||||||
|
let nextX = x + dragOffset.x + snapOffset.x;
|
||||||
|
let nextY = y + dragOffset.y + snapOffset.y;
|
||||||
|
|
||||||
|
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
||||||
|
const [nextGridX, nextGridY] = getGridPoint(
|
||||||
|
x + dragOffset.x,
|
||||||
|
y + dragOffset.y,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapOffset.x === 0) {
|
||||||
|
nextX = nextGridX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapOffset.y === 0) {
|
||||||
|
nextY = nextGridY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: nextX - x,
|
||||||
|
y: nextY - y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const updateElementCoords = (
|
const updateElementCoords = (
|
||||||
lockDirection: boolean,
|
|
||||||
distanceX: number,
|
|
||||||
distanceY: number,
|
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
offset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
) => {
|
) => {
|
||||||
let x: number;
|
const originalElement =
|
||||||
let y: number;
|
pointerDownState.originalElements.get(element.id) ?? element;
|
||||||
if (lockDirection) {
|
|
||||||
const lockX = lockDirection && distanceX < distanceY;
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
const lockY = lockDirection && distanceX > distanceY;
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
x,
|
x: nextX,
|
||||||
y,
|
y: nextY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDragOffsetXY = (
|
export const getDragOffsetXY = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
x: number,
|
x: number,
|
||||||
@@ -133,6 +148,10 @@ export const dragNewElement = (
|
|||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null,
|
widthAspectRatio?: number | null,
|
||||||
|
originOffset: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null = null,
|
||||||
) => {
|
) => {
|
||||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||||
if (widthAspectRatio) {
|
if (widthAspectRatio) {
|
||||||
@@ -173,8 +192,8 @@ export const dragNewElement = (
|
|||||||
|
|
||||||
if (width !== 0 && height !== 0) {
|
if (width !== 0 && height !== 0) {
|
||||||
mutateElement(draggingElement, {
|
mutateElement(draggingElement, {
|
||||||
x: newX,
|
x: newX + (originOffset?.x ?? 0),
|
||||||
y: newY,
|
y: newY + (originOffset?.y ?? 0),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { Point, PointerDownState } from "../types";
|
import { AppState, Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
@@ -79,6 +79,7 @@ export const transformElements = (
|
|||||||
pointerY: number,
|
pointerY: number,
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
@@ -466,8 +467,8 @@ export const resizeSingleElement = (
|
|||||||
boundTextElement.fontSize,
|
boundTextElement.fontSize,
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
eleNewWidth = Math.max(eleNewWidth, minWidth);
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
eleNewHeight = Math.max(eleNewHeight, minHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,8 +509,11 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flipX = eleNewWidth < 0;
|
||||||
|
const flipY = eleNewHeight < 0;
|
||||||
|
|
||||||
// Flip horizontally
|
// Flip horizontally
|
||||||
if (eleNewWidth < 0) {
|
if (flipX) {
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
||||||
}
|
}
|
||||||
@@ -517,8 +521,9 @@ export const resizeSingleElement = (
|
|||||||
newTopLeft[0] += Math.abs(newBoundsWidth);
|
newTopLeft[0] += Math.abs(newBoundsWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip vertically
|
// Flip vertically
|
||||||
if (eleNewHeight < 0) {
|
if (flipY) {
|
||||||
if (transformHandleDirection.includes("s")) {
|
if (transformHandleDirection.includes("s")) {
|
||||||
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
||||||
}
|
}
|
||||||
@@ -542,10 +547,20 @@ export const resizeSingleElement = (
|
|||||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -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
|
// Readjust points for linear elements
|
||||||
let rescaledElementPointsY;
|
let rescaledElementPointsY;
|
||||||
let rescaledPoints;
|
let rescaledPoints;
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
rescaledElementPointsY = rescalePoints(
|
rescaledElementPointsY = rescalePoints(
|
||||||
1,
|
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 = {
|
const resizedElement = {
|
||||||
width: Math.abs(eleNewWidth),
|
width: Math.abs(eleNewWidth),
|
||||||
height: Math.abs(eleNewHeight),
|
height: Math.abs(eleNewHeight),
|
||||||
x: newOrigin[0],
|
x: nextX,
|
||||||
y: newOrigin[1],
|
y: nextY,
|
||||||
points: rescaledPoints,
|
points: rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -680,6 +690,10 @@ export const resizeMultipleElements = (
|
|||||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||||
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// const originalHeight = maxY - minY;
|
||||||
|
// const originalWidth = maxX - minX;
|
||||||
|
|
||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const showSelectedShapeActions = (
|
|||||||
(appState.editingElement ||
|
(appState.editingElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
appState.activeTool.type !== "eraser" &&
|
appState.activeTool.type !== "eraser" &&
|
||||||
appState.activeTool.type !== "hand"))) ||
|
appState.activeTool.type !== "hand" &&
|
||||||
|
appState.activeTool.type !== "laser"))) ||
|
||||||
getSelectedElements(elements, appState).length),
|
getSelectedElements(elements, appState).length),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -957,7 +957,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
85,
|
85,
|
||||||
4.5,
|
4.999999999999986,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -1002,8 +1002,8 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
375,
|
374.99999999999994,
|
||||||
-539,
|
-535.0000000000001,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1190,7 +1190,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
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);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1200,9 +1200,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(156);
|
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||||
// cache updated again
|
// 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 () => {
|
it("should reset the container height cache when font properties updated", async () => {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2, frame]);
|
expectEqualIds([rect2, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements", async () => {
|
it.skip("should add elements", async () => {
|
||||||
h.elements = [rect2, rect3, frame];
|
h.elements = [rect2, rect3, frame];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -188,7 +188,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect3, rect2, frame]);
|
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];
|
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -199,7 +199,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
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];
|
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
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];
|
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
|
|||||||
132
src/frame.ts
132
src/frame.ts
@@ -14,7 +14,7 @@ import {
|
|||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
} from "./element/textElement";
|
} from "./element/textElement";
|
||||||
import { arrayToMap, findIndex } from "./utils";
|
import { arrayToMap } from "./utils";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
@@ -457,85 +457,87 @@ export const addElementsToFrame = (
|
|||||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) => {
|
) => {
|
||||||
const _elementsToAdd: ExcalidrawElement[] = [];
|
const currTargetFrameChildrenMap = new Map(
|
||||||
|
allElements.reduce(
|
||||||
for (const element of elementsToAdd) {
|
(acc: [ExcalidrawElement["id"], ExcalidrawElement][], element) => {
|
||||||
_elementsToAdd.push(element);
|
if (element.frameId === frame.id) {
|
||||||
|
acc.push([element.id, element]);
|
||||||
const boundTextElement = getBoundTextElement(element);
|
}
|
||||||
if (boundTextElement) {
|
return acc;
|
||||||
_elementsToAdd.push(boundTextElement);
|
},
|
||||||
}
|
[],
|
||||||
}
|
),
|
||||||
|
|
||||||
const allElementsIndex = allElements.reduce(
|
|
||||||
(acc: Record<string, number>, element, index) => {
|
|
||||||
acc[element.id] = index;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const frameIndex = allElementsIndex[frame.id];
|
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||||
// need to be calculated before the mutation below occurs
|
|
||||||
const leftFrameBoundaryIndex = findIndex(
|
|
||||||
allElements,
|
|
||||||
(e) => e.frameId === frame.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingFrameChildren = allElements.filter(
|
const finalElementsToAdd: ExcalidrawElement[] = [];
|
||||||
(element) => element.frameId === frame.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const addedFrameChildren_left: ExcalidrawElement[] = [];
|
|
||||||
const addedFrameChildren_right: 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(
|
for (const element of omitGroupsContainingFrames(
|
||||||
allElements,
|
allElements,
|
||||||
_elementsToAdd,
|
elementsToAdd,
|
||||||
)) {
|
)) {
|
||||||
if (element.frameId !== frame.id && !isFrameElement(element)) {
|
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||||
if (allElementsIndex[element.id] > frameIndex) {
|
finalElementsToAdd.push(element);
|
||||||
addedFrameChildren_right.push(element);
|
}
|
||||||
} else {
|
|
||||||
addedFrameChildren_left.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(
|
const boundTextElement = getBoundTextElement(element);
|
||||||
element,
|
if (
|
||||||
{
|
boundTextElement &&
|
||||||
frameId: frame.id,
|
!suppliedElementsToAddSet.has(boundTextElement.id) &&
|
||||||
},
|
!currTargetFrameChildrenMap.has(boundTextElement.id)
|
||||||
false,
|
) {
|
||||||
);
|
finalElementsToAdd.push(boundTextElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameElement = allElements[frameIndex];
|
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
|
||||||
const nextFrameChildren = addedFrameChildren_left
|
|
||||||
.concat(existingFrameChildren)
|
|
||||||
.concat(addedFrameChildren_right);
|
|
||||||
|
|
||||||
const nextFrameChildrenMap = nextFrameChildren.reduce(
|
const nextElements: ExcalidrawElement[] = [];
|
||||||
(acc: Record<string, boolean>, element) => {
|
|
||||||
acc[element.id] = true;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextOtherElements_left = allElements
|
const processedElements = new Set<ExcalidrawElement["id"]>();
|
||||||
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
|
|
||||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
|
||||||
|
|
||||||
const nextOtherElement_right = allElements
|
for (const element of allElements) {
|
||||||
.slice(frameIndex + 1)
|
if (processedElements.has(element.id)) {
|
||||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const nextElements = nextOtherElements_left
|
processedElements.add(element.id);
|
||||||
.concat(nextFrameChildren)
|
|
||||||
.concat([frameElement])
|
if (
|
||||||
.concat(nextOtherElement_right);
|
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;
|
return nextElements;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const CODES = {
|
|||||||
V: "KeyV",
|
V: "KeyV",
|
||||||
Z: "KeyZ",
|
Z: "KeyZ",
|
||||||
R: "KeyR",
|
R: "KeyR",
|
||||||
|
S: "KeyS",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@
|
|||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
"lightMode": "Light mode",
|
"lightMode": "Light mode",
|
||||||
"zenMode": "Zen mode",
|
"zenMode": "Zen mode",
|
||||||
|
"objectsSnapMode": "Snap to objects",
|
||||||
"exitZenMode": "Exit zen mode",
|
"exitZenMode": "Exit zen mode",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -235,6 +236,7 @@
|
|||||||
"eraser": "Eraser",
|
"eraser": "Eraser",
|
||||||
"frame": "Frame tool",
|
"frame": "Frame tool",
|
||||||
"embeddable": "Web Embed",
|
"embeddable": "Web Embed",
|
||||||
|
"laser": "Laser pointer",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools"
|
"extraTools": "More tools"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { rotate } from "./math";
|
import { rangeIntersection, rangesOverlap, rotate } from "./math";
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("rotate", () => {
|
||||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
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]);
|
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.
|
// angle, which we can check with modulo after rounding.
|
||||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
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.
|
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)
|
## 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)
|
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@excalidraw/excalidraw",
|
"name": "@excalidraw/excalidraw",
|
||||||
"version": "0.16.0",
|
"version": "0.16.1",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "types/packages/excalidraw/index.d.ts",
|
"types": "types/packages/excalidraw/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -22,5 +22,12 @@ const polyfill = () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Element.prototype.replaceChildren) {
|
||||||
|
Element.prototype.replaceChildren = function (...nodes) {
|
||||||
|
this.innerHTML = "";
|
||||||
|
this.append(...nodes);
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export default polyfill;
|
export default polyfill;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
|
import { renderSnaps } from "./renderSnaps";
|
||||||
import {
|
import {
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
@@ -720,6 +721,8 @@ const _renderInteractiveScene = ({
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSnaps(context, appState);
|
||||||
|
|
||||||
// Reset zoom
|
// Reset zoom
|
||||||
context.restore();
|
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,
|
getFrameElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import { isShallowEqual } from "../utils";
|
import { isShallowEqual } from "../utils";
|
||||||
|
import { isElementInViewport } from "../element/sizeHelpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames and their containing elements are not to be selected at the same time.
|
* Frames and their containing elements are not to be selected at the same time.
|
||||||
@@ -89,6 +90,26 @@ export const getElementsWithinSelection = (
|
|||||||
return elementsInSelection;
|
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
|
// FIXME move this into the editor instance to keep utility methods stateless
|
||||||
export const isSomeElementSelected = (function () {
|
export const isSomeElementSelected = (function () {
|
||||||
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -363,6 +368,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@@ -524,12 +530,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -553,6 +561,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -752,6 +763,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -1125,6 +1139,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -1498,6 +1515,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -1695,6 +1715,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -1933,6 +1956,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -2239,6 +2265,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -2623,6 +2652,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -3502,6 +3534,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -3875,6 +3910,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -4251,6 +4289,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -4983,6 +5024,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -5565,6 +5609,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`] = `
|
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,
|
"viewMode": true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checked": [Function],
|
||||||
|
"contextItemLabel": "buttons.objectsSnapMode",
|
||||||
|
"keyTest": [Function],
|
||||||
|
"name": "objectsSnapMode",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "canvas",
|
||||||
|
"predicate": [Function],
|
||||||
|
},
|
||||||
|
"viewMode": true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checked": [Function],
|
"checked": [Function],
|
||||||
"contextItemLabel": "buttons.zenMode",
|
"contextItemLabel": "buttons.zenMode",
|
||||||
@@ -6035,12 +6093,17 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -6062,6 +6125,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@@ -6431,12 +6495,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -6460,6 +6526,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@@ -6805,12 +6872,17 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -6834,6 +6906,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"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 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`;
|
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",
|
"gridMode",
|
||||||
"zenMode",
|
"zenMode",
|
||||||
"viewMode",
|
"viewMode",
|
||||||
|
"objectsSnapMode",
|
||||||
"stats",
|
"stats",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@@ -79,7 +79,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@@ -144,7 +144,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@@ -180,7 +180,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@@ -221,7 +221,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@@ -241,7 +241,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@@ -261,7 +261,7 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@@ -288,7 +288,7 @@ describe("Test dragCreate", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@@ -315,7 +315,7 @@ describe("Test dragCreate", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1048,14 +1048,14 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"height": 130,
|
"height": 130,
|
||||||
"width": 367,
|
"width": 366.11716195150507,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(getBoundTextElementPosition(container, textElement))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"x": 272,
|
"x": 271.11716195150507,
|
||||||
"y": 45,
|
"y": 45,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@@ -1069,9 +1069,9 @@ describe("Test Linear Elements", () => {
|
|||||||
[
|
[
|
||||||
20,
|
20,
|
||||||
35,
|
35,
|
||||||
502,
|
501.11716195150507,
|
||||||
95,
|
95,
|
||||||
205.9061448421403,
|
205.4589377083102,
|
||||||
52.5,
|
52.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe("move element", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -84,8 +84,8 @@ describe("move element", () => {
|
|||||||
// select the second rectangles
|
// select the second rectangles
|
||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(24);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(20);
|
expect(renderStaticScene).toHaveBeenCalledTimes(19);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
@@ -131,7 +131,7 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
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 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ describe("remove shape in non linear elements", () => {
|
|||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ describe("remove shape in non linear elements", () => {
|
|||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -110,8 +110,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
@@ -153,9 +153,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|||||||
@@ -55,10 +55,15 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@@ -80,6 +85,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -342,7 +342,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -374,7 +374,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -419,7 +419,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -463,7 +463,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
|||||||
81
src/types.ts
81
src/types.ts
@@ -18,7 +18,6 @@ import {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
import { SuggestedBinding } from "./element/binding";
|
import { SuggestedBinding } from "./element/binding";
|
||||||
@@ -34,15 +33,13 @@ import Library from "./data/library";
|
|||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
|
import { SnapLine } from "./snapping";
|
||||||
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
export type Collaborator = {
|
export type Collaborator = {
|
||||||
pointer?: {
|
pointer?: CollaboratorPointer;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
button?: "up" | "down";
|
button?: "up" | "down";
|
||||||
selectedElementIds?: AppState["selectedElementIds"];
|
selectedElementIds?: AppState["selectedElementIds"];
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
@@ -58,6 +55,12 @@ export type Collaborator = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CollaboratorPointer = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
tool: "pointer" | "laser";
|
||||||
|
};
|
||||||
|
|
||||||
export type DataURL = string & { _brand: "DataURL" };
|
export type DataURL = string & { _brand: "DataURL" };
|
||||||
|
|
||||||
export type BinaryFileData = {
|
export type BinaryFileData = {
|
||||||
@@ -85,21 +88,31 @@ export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
|
|||||||
|
|
||||||
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
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:
|
type: ToolType;
|
||||||
| typeof SHAPES[number]["value"]
|
|
||||||
| "eraser"
|
|
||||||
| "hand"
|
|
||||||
| "frame"
|
|
||||||
| "embeddable";
|
|
||||||
customType: null;
|
customType: null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "custom";
|
type: "custom";
|
||||||
customType: string;
|
customType: string;
|
||||||
}
|
};
|
||||||
| null;
|
|
||||||
|
|
||||||
export type SidebarName = string;
|
export type SidebarName = string;
|
||||||
export type SidebarTabName = string;
|
export type SidebarTabName = string;
|
||||||
@@ -150,6 +163,9 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
||||||
// Collaborators
|
// Collaborators
|
||||||
collaborators: AppState["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
|
* 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.
|
* currently active tool. At the moment applies to `eraser` and `hand` tool.
|
||||||
*/
|
*/
|
||||||
lastActiveTool: LastActiveTool;
|
lastActiveTool: ActiveTool | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
} & (
|
} & ActiveTool;
|
||||||
| {
|
|
||||||
type:
|
|
||||||
| typeof SHAPES[number]["value"]
|
|
||||||
| "eraser"
|
|
||||||
| "hand"
|
|
||||||
| "frame"
|
|
||||||
| "embeddable";
|
|
||||||
customType: null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "custom";
|
|
||||||
customType: string;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
penMode: boolean;
|
penMode: boolean;
|
||||||
penDetected: boolean;
|
penDetected: boolean;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
@@ -287,6 +289,13 @@ export type AppState = {
|
|||||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
|
|
||||||
|
snapLines: readonly SnapLine[];
|
||||||
|
originSnapOffset: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null;
|
||||||
|
objectsSnapModeEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
@@ -384,7 +393,7 @@ export interface ExcalidrawProps {
|
|||||||
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
||||||
isCollaborating?: boolean;
|
isCollaborating?: boolean;
|
||||||
onPointerUpdate?: (payload: {
|
onPointerUpdate?: (payload: {
|
||||||
pointer: { x: number; y: number };
|
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
pointersMap: Gesture["pointers"];
|
pointersMap: Gesture["pointers"];
|
||||||
}) => void;
|
}) => void;
|
||||||
@@ -400,6 +409,7 @@ export interface ExcalidrawProps {
|
|||||||
viewModeEnabled?: boolean;
|
viewModeEnabled?: boolean;
|
||||||
zenModeEnabled?: boolean;
|
zenModeEnabled?: boolean;
|
||||||
gridModeEnabled?: boolean;
|
gridModeEnabled?: boolean;
|
||||||
|
objectsSnapModeEnabled?: boolean;
|
||||||
libraryReturnUrl?: string;
|
libraryReturnUrl?: string;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -527,6 +537,8 @@ export type AppClassProperties = {
|
|||||||
onInsertElements: App["onInsertElements"];
|
onInsertElements: App["onInsertElements"];
|
||||||
onExportImage: App["onExportImage"];
|
onExportImage: App["onExportImage"];
|
||||||
lastViewportPosition: App["lastViewportPosition"];
|
lastViewportPosition: App["lastViewportPosition"];
|
||||||
|
togglePenMode: App["togglePenMode"];
|
||||||
|
setActiveTool: App["setActiveTool"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -653,3 +665,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,
|
FontString,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} 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 { unstable_batchedUpdates } from "react-dom";
|
||||||
import { SHAPES } from "./shapes";
|
|
||||||
import { isEraserActive, isHandToolActive } from "./appState";
|
import { isEraserActive, isHandToolActive } from "./appState";
|
||||||
import { ResolutionType } from "./utility-types";
|
import { ResolutionType } from "./utility-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -371,15 +370,10 @@ export const updateActiveTool = (
|
|||||||
appState: Pick<AppState, "activeTool">,
|
appState: Pick<AppState, "activeTool">,
|
||||||
data: (
|
data: (
|
||||||
| {
|
| {
|
||||||
type:
|
type: ToolType;
|
||||||
| typeof SHAPES[number]["value"]
|
|
||||||
| "eraser"
|
|
||||||
| "hand"
|
|
||||||
| "frame"
|
|
||||||
| "embeddable";
|
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { lastActiveToolBeforeEraser?: LastActiveTool },
|
) & { lastActiveToolBeforeEraser?: ActiveTool | null },
|
||||||
): AppState["activeTool"] => {
|
): AppState["activeTool"] => {
|
||||||
if (data.type === "custom") {
|
if (data.type === "custom") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
src: "apple-touch-icon.png",
|
src: "apple-touch-icon.png",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
sizes: "256x256",
|
sizes: "180x180",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
|
|||||||
@@ -1522,6 +1522,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd"
|
resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd"
|
||||||
integrity sha512-GemHNF5Z6ga0BWBSX7GJaNBUchLu6RwTcAB84eX1MeckRNhNasAsPCdelDlFalz27iS4RuYEQh0bPE8SRxJgbQ==
|
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":
|
"@excalidraw/prettier-config@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
|
||||||
|
|||||||
Reference in New Issue
Block a user