mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
merge with master
This commit is contained in:
commit
e8e97adace
86 changed files with 4355 additions and 1113 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||||
|
|
||||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ function App() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
This will only work for `Desktop` devices.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ All `props` are _optional_.
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ export default function ExampleApp({
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
|
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
|
@ -192,6 +193,7 @@ export default function ExampleApp({
|
||||||
}) => setPointerData(payload),
|
}) => setPointerData(payload),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
renderScrollbars,
|
||||||
gridModeEnabled,
|
gridModeEnabled,
|
||||||
theme,
|
theme,
|
||||||
name: "Custom name of drawing",
|
name: "Custom name of drawing",
|
||||||
|
@ -710,6 +712,14 @@ export default function ExampleApp({
|
||||||
/>
|
/>
|
||||||
Grid mode
|
Grid mode
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={renderScrollbars}
|
||||||
|
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||||
|
/>
|
||||||
|
Render scrollbars
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:preview": "yarn build && vite preview --port 5002",
|
"preview": "vite preview --port 5002",
|
||||||
|
"build:preview": "yarn build && yarn preview",
|
||||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,10 @@ export default defineConfig(({ mode }) => {
|
||||||
alias: [
|
alias: [
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/common$/,
|
find: /^@excalidraw\/common$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/common/src/index.ts"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/common/src/index.ts",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/common\/(.*?)/,
|
find: /^@excalidraw\/common\/(.*?)/,
|
||||||
|
@ -33,7 +36,10 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/element$/,
|
find: /^@excalidraw\/element$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/element/src/index.ts"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/element/src/index.ts",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/element\/(.*?)/,
|
find: /^@excalidraw\/element\/(.*?)/,
|
||||||
|
@ -41,7 +47,10 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw$/,
|
find: /^@excalidraw\/excalidraw$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/excalidraw/index.tsx",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
||||||
|
@ -57,7 +66,10 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/utils$/,
|
find: /^@excalidraw\/utils$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/src/index.ts"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/utils/src/index.ts",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/utils\/(.*?)/,
|
find: /^@excalidraw\/utils\/(.*?)/,
|
||||||
|
@ -213,7 +225,7 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
id:"excalidraw",
|
id: "excalidraw",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
theme_color: "#121212",
|
theme_color: "#121212",
|
||||||
background_color: "#ffffff",
|
background_color: "#ffffff",
|
||||||
|
|
|
@ -2,6 +2,8 @@ import oc from "open-color";
|
||||||
|
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
|
||||||
|
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
source: R,
|
source: R,
|
||||||
|
|
|
@ -112,6 +112,7 @@ export const YOUTUBE_STATES = {
|
||||||
export const ENV = {
|
export const ENV = {
|
||||||
TEST: "test",
|
TEST: "test",
|
||||||
DEVELOPMENT: "development",
|
DEVELOPMENT: "development",
|
||||||
|
PRODUCTION: "production",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
|
@ -318,6 +319,9 @@ export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
export const SVG_DOCUMENT_PREAMBLE = `<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
`;
|
||||||
|
|
||||||
export const ENCRYPTION_KEY_BITS = 128;
|
export const ENCRYPTION_KEY_BITS = 128;
|
||||||
|
|
||||||
|
@ -419,6 +423,7 @@ export const LIBRARY_DISABLED_TYPES = new Set([
|
||||||
// use these constants to easily identify reference sites
|
// use these constants to easily identify reference sites
|
||||||
export const TOOL_TYPE = {
|
export const TOOL_TYPE = {
|
||||||
selection: "selection",
|
selection: "selection",
|
||||||
|
lasso: "lasso",
|
||||||
rectangle: "rectangle",
|
rectangle: "rectangle",
|
||||||
diamond: "diamond",
|
diamond: "diamond",
|
||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { average } from "@excalidraw/math";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
|
ExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -385,7 +386,7 @@ export const updateActiveTool = (
|
||||||
type: ToolType;
|
type: ToolType;
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean }) & {
|
) & { locked?: boolean; fromSelection?: boolean }) & {
|
||||||
lastActiveToolBeforeEraser?: ActiveTool | null;
|
lastActiveToolBeforeEraser?: ActiveTool | null;
|
||||||
},
|
},
|
||||||
): AppState["activeTool"] => {
|
): AppState["activeTool"] => {
|
||||||
|
@ -407,6 +408,7 @@ export const updateActiveTool = (
|
||||||
type: data.type,
|
type: data.type,
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: data.locked ?? appState.activeTool.locked,
|
locked: data.locked ?? appState.activeTool.locked,
|
||||||
|
fromSelection: data.fromSelection ?? false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -737,6 +739,8 @@ export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
|
|
||||||
|
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||||
|
|
||||||
export const isServerEnv = () =>
|
export const isServerEnv = () =>
|
||||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||||
|
|
||||||
|
@ -1200,3 +1204,32 @@ export const escapeDoubleQuotes = (str: string) => {
|
||||||
|
|
||||||
export const castArray = <T>(value: T | T[]): T[] =>
|
export const castArray = <T>(value: T | T[]): T[] =>
|
||||||
Array.isArray(value) ? value : [value];
|
Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
export const elementCenterPoint = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
xOffset: number = 0,
|
||||||
|
yOffset: number = 0,
|
||||||
|
) => {
|
||||||
|
const { x, y, width, height } = element;
|
||||||
|
|
||||||
|
const centerXPoint = x + width / 2 + xOffset;
|
||||||
|
|
||||||
|
const centerYPoint = y + height / 2 + yOffset;
|
||||||
|
|
||||||
|
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
|
export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
||||||
|
return Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sizeOf = (
|
||||||
|
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
|
||||||
|
): number => {
|
||||||
|
return isReadonlyArray(value)
|
||||||
|
? value.length
|
||||||
|
: value instanceof Map
|
||||||
|
? value.size
|
||||||
|
: Object.keys(value).length;
|
||||||
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
invariant,
|
invariant,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
elementCenterPoint,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -903,13 +904,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(
|
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||||
p,
|
|
||||||
pointFrom<GlobalPoint>(
|
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1039,10 +1034,7 @@ export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
|
@ -1139,10 +1131,9 @@ export const snapToMid = (
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = pointFrom<GlobalPoint>(
|
|
||||||
x + width / 2 - 0.1,
|
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||||
y + height / 2 - 0.1,
|
|
||||||
);
|
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
|
@ -1227,10 +1218,7 @@ const updateBoundPoint = (
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
|
@ -1274,10 +1262,7 @@ const updateBoundPoint = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const interceptorLength =
|
const interceptorLength =
|
||||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||||
pointDistance(adjacentPoint, center) +
|
pointDistance(adjacentPoint, center) +
|
||||||
|
@ -1422,20 +1407,20 @@ const getLinearElementEdgeCoors = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fixBindingsAfterDuplication = (
|
export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
newElements: ExcalidrawElement[],
|
duplicatedElements: ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
duplicateElementsMap: NonDeletedSceneElementsMap,
|
||||||
) => {
|
) => {
|
||||||
for (const element of newElements) {
|
for (const duplicateElement of duplicatedElements) {
|
||||||
if ("boundElements" in element && element.boundElements) {
|
if ("boundElements" in duplicateElement && duplicateElement.boundElements) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
boundElements: element.boundElements.reduce(
|
boundElements: duplicateElement.boundElements.reduce(
|
||||||
(
|
(
|
||||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||||
binding,
|
binding,
|
||||||
) => {
|
) => {
|
||||||
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
const newBindingId = origIdToDuplicateId.get(binding.id);
|
||||||
if (newBindingId) {
|
if (newBindingId) {
|
||||||
acc.push({ ...binding, id: newBindingId });
|
acc.push({ ...binding, id: newBindingId });
|
||||||
}
|
}
|
||||||
|
@ -1446,46 +1431,47 @@ export const fixBindingsAfterDuplication = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("containerId" in element && element.containerId) {
|
if ("containerId" in duplicateElement && duplicateElement.containerId) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
containerId:
|
||||||
|
origIdToDuplicateId.get(duplicateElement.containerId) ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("endBinding" in element && element.endBinding) {
|
if ("endBinding" in duplicateElement && duplicateElement.endBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.endBinding.elementId,
|
duplicateElement.endBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
endBinding: newEndBindingId
|
endBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.endBinding,
|
...duplicateElement.endBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ("startBinding" in element && element.startBinding) {
|
if ("startBinding" in duplicateElement && duplicateElement.startBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.startBinding.elementId,
|
duplicateElement.startBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
startBinding: newEndBindingId
|
startBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.startBinding,
|
...duplicateElement.startBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(duplicateElement)) {
|
||||||
Object.assign(
|
Object.assign(
|
||||||
element,
|
duplicateElement,
|
||||||
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
updateElbowArrowPoints(duplicateElement, duplicateElementsMap, {
|
||||||
points: [
|
points: [
|
||||||
element.points[0],
|
duplicateElement.points[0],
|
||||||
element.points[element.points.length - 1],
|
duplicateElement.points[duplicateElement.points.length - 1],
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1580,10 +1566,7 @@ const determineFocusDistance = (
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pointsEqual(a, b)) {
|
if (pointsEqual(a, b)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1713,10 +1696,7 @@ const determineFocusPoint = (
|
||||||
focus: number,
|
focus: number,
|
||||||
adjacentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
return center;
|
return center;
|
||||||
|
@ -2147,10 +2127,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
pointFrom<GlobalPoint>(
|
elementCenterPoint(element),
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
),
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
import {
|
||||||
|
rescalePoints,
|
||||||
|
arrayToMap,
|
||||||
|
invariant,
|
||||||
|
sizeOf,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
|
@ -13,7 +18,10 @@ import {
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Curve,
|
||||||
Degrees,
|
Degrees,
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
LineSegment,
|
LineSegment,
|
||||||
|
@ -37,6 +45,13 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import { getElementShape } from "./shapes";
|
||||||
|
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -45,6 +60,9 @@ import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -254,50 +272,82 @@ export const getElementAbsoluteCoords = (
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Given an element, return the line segments that make up the element.
|
||||||
|
*
|
||||||
|
* Uses helpers from /math
|
||||||
|
*/
|
||||||
export const getElementLineSegments = (
|
export const getElementLineSegments = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): LineSegment<GlobalPoint>[] => {
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const shape = getElementShape(element, elementsMap);
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
const center: GlobalPoint = pointFrom(cx, cy);
|
if (shape.type === "polycurve") {
|
||||||
|
const curves = shape.data;
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
const points = curves
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||||
|
.flat();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
while (i < element.points.length - 1) {
|
while (i < points.length - 1) {
|
||||||
segments.push(
|
segments.push(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(
|
pointFrom(points[i][0], points[i][1]),
|
||||||
pointFrom(
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
element.points[i][0] + element.x,
|
|
||||||
element.points[i][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
pointRotateRads(
|
|
||||||
pointFrom(
|
|
||||||
element.points[i + 1][0] + element.x,
|
|
||||||
element.points[i + 1][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
|
} else if (shape.type === "polyline") {
|
||||||
|
return shape.data as LineSegment<GlobalPoint>[];
|
||||||
|
} else if (_isRectanguloidElement(element)) {
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||||
|
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
|
const [sides, corners] = deconstructDiamondElement(element);
|
||||||
|
const cornerSegments = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (shape.type === "polygon") {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
if (container && isLinearElement(container)) {
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [
|
||||||
|
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||||
|
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||||
|
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||||
|
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||||
|
];
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nw, ne, sw, se, n, s, w, e] = (
|
const points = shape.data as GlobalPoint[];
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
} else if (shape.type === "ellipse") {
|
||||||
|
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nw, ne, sw, se, , , w, e] = (
|
||||||
[
|
[
|
||||||
[x1, y1],
|
[x1, y1],
|
||||||
[x2, y1],
|
[x2, y1],
|
||||||
|
@ -310,28 +360,6 @@ export const getElementLineSegments = (
|
||||||
] as GlobalPoint[]
|
] as GlobalPoint[]
|
||||||
).map((point) => pointRotateRads(point, center, element.angle));
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
lineSegment(nw, ne),
|
lineSegment(nw, ne),
|
||||||
lineSegment(sw, se),
|
lineSegment(sw, se),
|
||||||
|
@ -344,6 +372,94 @@ export const getElementLineSegments = (
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _isRectanguloidElement = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawRectanguloidElement => {
|
||||||
|
return (
|
||||||
|
element != null &&
|
||||||
|
(element.type === "rectangle" ||
|
||||||
|
element.type === "image" ||
|
||||||
|
element.type === "iframe" ||
|
||||||
|
element.type === "embeddable" ||
|
||||||
|
element.type === "frame" ||
|
||||||
|
element.type === "magicframe" ||
|
||||||
|
(element.type === "text" && !element.containerId))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotatedSides = (
|
||||||
|
sides: LineSegment<GlobalPoint>[],
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
) => {
|
||||||
|
return sides.map((side) => {
|
||||||
|
return lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||||
|
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnCurve = (
|
||||||
|
curve: Curve<GlobalPoint>,
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const points = pointsOnBezierCurves(curve, 10);
|
||||||
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
while (i < points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i][0], points[i][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnEllipse = (
|
||||||
|
ellipse: ExcalidrawEllipseElement,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
ellipse.x + ellipse.width / 2,
|
||||||
|
ellipse.y + ellipse.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const a = ellipse.width / 2;
|
||||||
|
const b = ellipse.height / 2;
|
||||||
|
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
const points: GlobalPoint[] = [];
|
||||||
|
const n = 90;
|
||||||
|
const deltaT = (Math.PI * 2) / n;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const t = i * deltaT;
|
||||||
|
const x = center[0] + a * Math.cos(t);
|
||||||
|
const y = center[1] + b * Math.sin(t);
|
||||||
|
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||||
*
|
*
|
||||||
|
@ -828,10 +944,10 @@ export const getElementBounds = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
elementsMap?: ElementsMap,
|
elementsMap?: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!sizeOf(elements)) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { isTransparent } from "@excalidraw/common";
|
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
import { getPolygonShape } from "@excalidraw/utils/shape";
|
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
|
@ -26,8 +26,6 @@ import type {
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||||
|
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
|
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { type Point } from "points-on-curve";
|
import { type Point } from "points-on-curve";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
elementCenterPoint(element),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {
|
import {
|
||||||
curvePointDistance,
|
curvePointDistance,
|
||||||
distanceToLineSegment,
|
distanceToLineSegment,
|
||||||
pointFrom,
|
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, -element.angle as Radians),
|
pointRotateRads(p, center, -element.angle as Radians),
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
|
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
|
||||||
import { fixBindingsAfterDuplication } from "./binding";
|
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -57,16 +57,14 @@ import type {
|
||||||
* multiple elements at once, share this map
|
* multiple elements at once, share this map
|
||||||
* amongst all of them
|
* amongst all of them
|
||||||
* @param element Element to duplicate
|
* @param element Element to duplicate
|
||||||
* @param overrides Any element properties to override
|
|
||||||
*/
|
*/
|
||||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
editingGroupId: AppState["editingGroupId"],
|
editingGroupId: AppState["editingGroupId"],
|
||||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||||
element: TElement,
|
element: TElement,
|
||||||
overrides?: Partial<TElement>,
|
|
||||||
randomizeSeed?: boolean,
|
randomizeSeed?: boolean,
|
||||||
): Readonly<TElement> => {
|
): Readonly<TElement> => {
|
||||||
let copy = deepCopyElement(element);
|
const copy = deepCopyElement(element);
|
||||||
|
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
__test__defineOrigId(copy, element.id);
|
__test__defineOrigId(copy, element.id);
|
||||||
|
@ -89,9 +87,6 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
return groupIdMapForOperation.get(groupId)!;
|
return groupIdMapForOperation.get(groupId)!;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (overrides) {
|
|
||||||
copy = Object.assign(copy, overrides);
|
|
||||||
}
|
|
||||||
return copy;
|
return copy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,9 +94,14 @@ export const duplicateElements = (
|
||||||
opts: {
|
opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
randomizeSeed?: boolean;
|
randomizeSeed?: boolean;
|
||||||
overrides?: (
|
overrides?: (data: {
|
||||||
originalElement: ExcalidrawElement,
|
duplicateElement: ExcalidrawElement;
|
||||||
) => Partial<ExcalidrawElement>;
|
origElement: ExcalidrawElement;
|
||||||
|
origIdToDuplicateId: Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>;
|
||||||
|
}) => Partial<ExcalidrawElement>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
|
@ -129,14 +129,6 @@ export const duplicateElements = (
|
||||||
editingGroupId: AppState["editingGroupId"];
|
editingGroupId: AppState["editingGroupId"];
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* If true, duplicated elements are inserted _before_ specified
|
|
||||||
* elements. Case: alt-dragging elements to duplicate them.
|
|
||||||
*
|
|
||||||
* TODO: remove this once (if) we stop replacing the original element
|
|
||||||
* with the duplicated one in the scene array.
|
|
||||||
*/
|
|
||||||
reverseOrder: boolean;
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
|
@ -150,8 +142,6 @@ export const duplicateElements = (
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
|
||||||
|
|
||||||
// Ids of elements that have already been processed so we don't push them
|
// Ids of elements that have already been processed so we don't push them
|
||||||
// into the array twice if we end up backtracking when retrieving
|
// into the array twice if we end up backtracking when retrieving
|
||||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
@ -164,10 +154,17 @@ export const duplicateElements = (
|
||||||
// loop over them.
|
// loop over them.
|
||||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const newElements: ExcalidrawElement[] = [];
|
const duplicatedElements: ExcalidrawElement[] = [];
|
||||||
const oldElements: ExcalidrawElement[] = [];
|
const origElements: ExcalidrawElement[] = [];
|
||||||
const oldIdToDuplicatedId = new Map();
|
const origIdToDuplicateId = new Map<
|
||||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>();
|
||||||
|
const duplicateIdToOrigElement = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>();
|
||||||
|
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||||
const _idsOfElementsToDuplicate =
|
const _idsOfElementsToDuplicate =
|
||||||
opts.type === "in-place"
|
opts.type === "in-place"
|
||||||
|
@ -185,7 +182,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
elements = normalizeElementOrder(elements);
|
elements = normalizeElementOrder(elements);
|
||||||
|
|
||||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
@ -211,17 +208,17 @@ export const duplicateElements = (
|
||||||
appState.editingGroupId,
|
appState.editingGroupId,
|
||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
opts.overrides?.(element),
|
|
||||||
opts.randomizeSeed,
|
opts.randomizeSeed,
|
||||||
);
|
);
|
||||||
|
|
||||||
processedIds.set(newElement.id, true);
|
processedIds.set(newElement.id, true);
|
||||||
|
|
||||||
duplicatedElementsMap.set(newElement.id, newElement);
|
duplicateElementsMap.set(newElement.id, newElement);
|
||||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
origIdToDuplicateId.set(element.id, newElement.id);
|
||||||
|
duplicateIdToOrigElement.set(newElement.id, element);
|
||||||
|
|
||||||
oldElements.push(element);
|
origElements.push(element);
|
||||||
newElements.push(newElement);
|
duplicatedElements.push(newElement);
|
||||||
|
|
||||||
acc.push(newElement);
|
acc.push(newElement);
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -245,21 +242,12 @@ export const duplicateElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseOrder && index < 1) {
|
if (index > elementsWithDuplicates.length - 1) {
|
||||||
elementsWithClones.unshift(...castArray(elements));
|
elementsWithDuplicates.push(...castArray(elements));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||||
elementsWithClones.push(...castArray(elements));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
elementsWithClones.splice(
|
|
||||||
index + (reverseOrder ? 0 : 1),
|
|
||||||
0,
|
|
||||||
...castArray(elements),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const frameIdsToDuplicate = new Set(
|
const frameIdsToDuplicate = new Set(
|
||||||
|
@ -291,11 +279,7 @@ export const duplicateElements = (
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetIndex = reverseOrder
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
? elementsWithClones.findIndex((el) => {
|
|
||||||
return el.groupIds?.includes(groupId);
|
|
||||||
})
|
|
||||||
: findLastIndex(elementsWithClones, (el) => {
|
|
||||||
return el.groupIds?.includes(groupId);
|
return el.groupIds?.includes(groupId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -315,7 +299,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
const frameChildren = getFrameChildren(elements, frameId);
|
const frameChildren = getFrameChildren(elements, frameId);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.frameId === frameId || el.id === frameId;
|
return el.frameId === frameId || el.id === frameId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -332,7 +316,7 @@ export const duplicateElements = (
|
||||||
if (hasBoundTextElement(element)) {
|
if (hasBoundTextElement(element)) {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return (
|
return (
|
||||||
el.id === element.id ||
|
el.id === element.id ||
|
||||||
("containerId" in el && el.containerId === element.id)
|
("containerId" in el && el.containerId === element.id)
|
||||||
|
@ -341,7 +325,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
targetIndex + (reverseOrder ? -1 : 0),
|
targetIndex,
|
||||||
copyElements([element, boundTextElement]),
|
copyElements([element, boundTextElement]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -354,7 +338,7 @@ export const duplicateElements = (
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
const container = getContainerElement(element, elementsMap);
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.id === element.id || el.id === container?.id;
|
return el.id === element.id || el.id === container?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -374,28 +358,46 @@ export const duplicateElements = (
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||||
copyElements(element),
|
copyElements(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fixBindingsAfterDuplication(
|
fixDuplicatedBindingsAfterDuplication(
|
||||||
newElements,
|
duplicatedElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindElementsToFramesAfterDuplication(
|
bindElementsToFramesAfterDuplication(
|
||||||
elementsWithClones,
|
elementsWithDuplicates,
|
||||||
oldElements,
|
origElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (opts.overrides) {
|
||||||
|
for (const duplicateElement of duplicatedElements) {
|
||||||
|
const origElement = duplicateIdToOrigElement.get(duplicateElement.id);
|
||||||
|
if (origElement) {
|
||||||
|
Object.assign(
|
||||||
|
duplicateElement,
|
||||||
|
opts.overrides({
|
||||||
|
duplicateElement,
|
||||||
|
origElement,
|
||||||
|
origIdToDuplicateId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newElements,
|
duplicatedElements,
|
||||||
elementsWithClones,
|
duplicateElementsMap,
|
||||||
|
elementsWithDuplicates,
|
||||||
|
origIdToDuplicateId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,33 +41,31 @@ import type {
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
nextElements: readonly ExcalidrawElement[],
|
nextElements: readonly ExcalidrawElement[],
|
||||||
oldElements: readonly ExcalidrawElement[],
|
origElements: readonly ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
) => {
|
) => {
|
||||||
const nextElementMap = arrayToMap(nextElements) as Map<
|
const nextElementMap = arrayToMap(nextElements) as Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
>;
|
>;
|
||||||
|
|
||||||
for (const element of oldElements) {
|
for (const element of origElements) {
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
// use its frameId to get the new frameId
|
// use its frameId to get the new frameId
|
||||||
const nextElementId = oldIdToDuplicatedId.get(element.id);
|
const nextElementId = origIdToDuplicateId.get(element.id);
|
||||||
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
|
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||||
if (nextElementId) {
|
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||||
const nextElement = nextElementMap.get(nextElementId);
|
|
||||||
if (nextElement) {
|
if (nextElement) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
nextElement,
|
nextElement,
|
||||||
{
|
{
|
||||||
frameId: nextFrameId ?? element.frameId,
|
frameId: nextFrameId ?? null,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isElementIntersectingFrame(
|
export function isElementIntersectingFrame(
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
normalizeRadians,
|
|
||||||
pointFrom,
|
pointFrom,
|
||||||
|
pointFromVector,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointScaleFromOrigin,
|
pointScaleFromOrigin,
|
||||||
radiansToDegrees,
|
pointsEqual,
|
||||||
triangleIncludesPoint,
|
triangleIncludesPoint,
|
||||||
|
vectorCross,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -13,7 +17,6 @@ import type {
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
Triangle,
|
Triangle,
|
||||||
Vector,
|
Vector,
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||||
|
@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
|
||||||
export const HEADING_UP = [0, -1] as Heading;
|
export const HEADING_UP = [0, -1] as Heading;
|
||||||
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
||||||
|
|
||||||
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
|
|
||||||
a: Point,
|
|
||||||
b: Point,
|
|
||||||
) => {
|
|
||||||
const angle = radiansToDegrees(
|
|
||||||
normalizeRadians(Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (angle >= 315 || angle < 45) {
|
|
||||||
return HEADING_UP;
|
|
||||||
} else if (angle >= 45 && angle < 135) {
|
|
||||||
return HEADING_RIGHT;
|
|
||||||
} else if (angle >= 135 && angle < 225) {
|
|
||||||
return HEADING_DOWN;
|
|
||||||
}
|
|
||||||
return HEADING_LEFT;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const vectorToHeading = (vec: Vector): Heading => {
|
export const vectorToHeading = (vec: Vector): Heading => {
|
||||||
const [x, y] = vec;
|
const [x, y] = vec;
|
||||||
const absX = Math.abs(x);
|
const absX = Math.abs(x);
|
||||||
|
@ -76,6 +61,165 @@ export const headingIsHorizontal = (a: Heading) =>
|
||||||
|
|
||||||
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
||||||
|
|
||||||
|
const headingForPointFromDiamondElement = (
|
||||||
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
|
aabb: Readonly<Bounds>,
|
||||||
|
point: Readonly<GlobalPoint>,
|
||||||
|
): Heading => {
|
||||||
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
|
if (isDevEnv() || isTestEnv()) {
|
||||||
|
invariant(
|
||||||
|
element.width > 0 && element.height > 0,
|
||||||
|
"Diamond element has no width or height",
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!pointsEqual(midPoint, point),
|
||||||
|
"The point is too close to the element mid point to determine heading",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHRINK = 0.95; // Rounded elements tolerance
|
||||||
|
const top = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const right = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const bottom = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const left = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
if (
|
||||||
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
|
||||||
|
0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(top, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, right),
|
||||||
|
vectorFromPoint(right, bottom),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(right, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, bottom),
|
||||||
|
vectorFromPoint(bottom, left),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, bottom),
|
||||||
|
vectorFromPoint(bottom, right),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(bottom, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
|
||||||
|
0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(left, midPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sides
|
||||||
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(top, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(right, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? top : right;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(right, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(bottom, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? bottom : right;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(bottom, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(left, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? bottom : left;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = element.width > element.height ? top : left;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
};
|
||||||
|
|
||||||
// Gets the heading for the point by creating a bounding box around the rotated
|
// Gets the heading for the point by creating a bounding box around the rotated
|
||||||
// close fitting bounding box, then creating 4 search cones around the center of
|
// close fitting bounding box, then creating 4 search cones around the center of
|
||||||
// the external bbox.
|
// the external bbox.
|
||||||
|
@ -89,74 +233,7 @@ export const headingForPointFromElement = <Point extends GlobalPoint>(
|
||||||
const midPoint = getCenterForBounds(aabb);
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
if (p[0] < element.x) {
|
return headingForPointFromDiamondElement(element, aabb, p);
|
||||||
return HEADING_LEFT;
|
|
||||||
} else if (p[1] < element.y) {
|
|
||||||
return HEADING_UP;
|
|
||||||
} else if (p[0] > element.x + element.width) {
|
|
||||||
return HEADING_RIGHT;
|
|
||||||
} else if (p[1] > element.y + element.height) {
|
|
||||||
return HEADING_DOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
const top = pointRotateRads(
|
|
||||||
pointScaleFromOrigin(
|
|
||||||
pointFrom(element.x + element.width / 2, element.y),
|
|
||||||
midPoint,
|
|
||||||
SEARCH_CONE_MULTIPLIER,
|
|
||||||
),
|
|
||||||
midPoint,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const right = pointRotateRads(
|
|
||||||
pointScaleFromOrigin(
|
|
||||||
pointFrom(element.x + element.width, element.y + element.height / 2),
|
|
||||||
midPoint,
|
|
||||||
SEARCH_CONE_MULTIPLIER,
|
|
||||||
),
|
|
||||||
midPoint,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const bottom = pointRotateRads(
|
|
||||||
pointScaleFromOrigin(
|
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height),
|
|
||||||
midPoint,
|
|
||||||
SEARCH_CONE_MULTIPLIER,
|
|
||||||
),
|
|
||||||
midPoint,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const left = pointRotateRads(
|
|
||||||
pointScaleFromOrigin(
|
|
||||||
pointFrom(element.x, element.y + element.height / 2),
|
|
||||||
midPoint,
|
|
||||||
SEARCH_CONE_MULTIPLIER,
|
|
||||||
),
|
|
||||||
midPoint,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p)
|
|
||||||
) {
|
|
||||||
return headingForDiamond(top, right);
|
|
||||||
} else if (
|
|
||||||
triangleIncludesPoint<Point>(
|
|
||||||
[right, bottom, midPoint] as Triangle<Point>,
|
|
||||||
p,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return headingForDiamond(right, bottom);
|
|
||||||
} else if (
|
|
||||||
triangleIncludesPoint<Point>(
|
|
||||||
[bottom, left, midPoint] as Triangle<Point>,
|
|
||||||
p,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return headingForDiamond(bottom, left);
|
|
||||||
}
|
|
||||||
|
|
||||||
return headingForDiamond(left, top);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const topLeft = pointScaleFromOrigin(
|
const topLeft = pointScaleFromOrigin(
|
||||||
|
|
|
@ -133,6 +133,7 @@ export class LinearElementEditor {
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
|
LinearElementEditor.normalizePoints(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
|
|
|
@ -7,13 +7,20 @@ import type {
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
import { isElementInViewport } from "./sizeHelpers";
|
import { isElementInViewport } from "./sizeHelpers";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
} from "./frame";
|
} from "./frame";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { selectGroupsForSelectedElements } from "./groups";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
|
@ -254,3 +261,48 @@ export const makeNextSelectedElementIds = (
|
||||||
|
|
||||||
return nextSelectedElementIds;
|
return nextSelectedElementIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _getLinearElementEditor = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const linears = targetElements.filter(isLinearElement);
|
||||||
|
if (linears.length === 1) {
|
||||||
|
const linear = linears[0];
|
||||||
|
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||||
|
const onlySingleLinearSelected = targetElements.every(
|
||||||
|
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onlySingleLinearSelected) {
|
||||||
|
return new LinearElementEditor(linear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectionStateForElements = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
selectedLinearElement: _getLinearElementEditor(targetElements),
|
||||||
|
...selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||||
|
targetElements,
|
||||||
|
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
|
if (!isBoundToContainer(element)) {
|
||||||
|
acc[element.id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
invariant,
|
invariant,
|
||||||
|
elementCenterPoint,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
|
@ -297,7 +298,7 @@ export const aabbForElement = (
|
||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = pointFrom(bbox.midX, bbox.midY);
|
const center = elementCenterPoint(element);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const showSelectedShapeActions = (
|
||||||
((appState.activeTool.type !== "custom" &&
|
((appState.activeTool.type !== "custom" &&
|
||||||
(appState.editingTextElement ||
|
(appState.editingTextElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
|
appState.activeTool.type !== "lasso" &&
|
||||||
appState.activeTool.type !== "eraser" &&
|
appState.activeTool.type !== "eraser" &&
|
||||||
appState.activeTool.type !== "hand" &&
|
appState.activeTool.type !== "hand" &&
|
||||||
appState.activeTool.type !== "laser"))) ||
|
appState.activeTool.type !== "laser"))) ||
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
isProdEnv,
|
||||||
|
invariant,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
@ -26,6 +28,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { Radians } from "../../math/src";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -44,13 +48,25 @@ export const redrawTextBoundingBox = (
|
||||||
informMutation = true,
|
informMutation = true,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
|
if (!isProdEnv()) {
|
||||||
|
invariant(
|
||||||
|
!container || !isArrowElement(container) || textElement.angle === 0,
|
||||||
|
"text element angle must be 0 if bound to arrow container",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextUpdates = {
|
const boundTextUpdates = {
|
||||||
x: textElement.x,
|
x: textElement.x,
|
||||||
y: textElement.y,
|
y: textElement.y,
|
||||||
text: textElement.text,
|
text: textElement.text,
|
||||||
width: textElement.width,
|
width: textElement.width,
|
||||||
height: textElement.height,
|
height: textElement.height,
|
||||||
angle: container?.angle ?? textElement.angle,
|
angle: (container
|
||||||
|
? isArrowElement(container)
|
||||||
|
? 0
|
||||||
|
: container.angle
|
||||||
|
: textElement.angle) as Radians,
|
||||||
};
|
};
|
||||||
|
|
||||||
boundTextUpdates.text = textElement.text;
|
boundTextUpdates.text = textElement.text;
|
||||||
|
@ -335,7 +351,10 @@ export const getTextElementAngle = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawTextContainer | null,
|
container: ExcalidrawTextContainer | null,
|
||||||
) => {
|
) => {
|
||||||
if (!container || isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!container) {
|
||||||
return textElement.angle;
|
return textElement.angle;
|
||||||
}
|
}
|
||||||
return container.angle;
|
return container.angle;
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCornerRadius } from "./shapes";
|
import { getCornerRadius } from "./shapes";
|
||||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
||||||
return [sides, []];
|
return [sides, []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
||||||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
pointFrom(element.x + topX, element.y + topY),
|
pointFrom(element.x + topX, element.y + topY),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -14,7 +13,7 @@ import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
|
@ -67,7 +66,7 @@ describe("duplicating single elements", () => {
|
||||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element, undefined, true);
|
const copy = duplicateElement(null, new Map(), element, true);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
@ -173,7 +172,7 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
});
|
});
|
||||||
|
@ -181,10 +180,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// generic id in-equality checks
|
// generic id in-equality checks
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
expect(origElements.map((e) => e.type)).toEqual(
|
expect(origElements.map((e) => e.type)).toEqual(
|
||||||
clonedElements.map((e) => e.type),
|
duplicatedElements.map((e) => e.type),
|
||||||
);
|
);
|
||||||
origElements.forEach((origElement, idx) => {
|
origElements.forEach((origElement, idx) => {
|
||||||
const clonedElement = clonedElements[idx];
|
const clonedElement = duplicatedElements[idx];
|
||||||
expect(origElement).toEqual(
|
expect(origElement).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.not.stringMatching(clonedElement.id),
|
id: expect.not.stringMatching(clonedElement.id),
|
||||||
|
@ -217,12 +216,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
const clonedArrows = clonedElements.filter(
|
const clonedArrows = duplicatedElements.filter(
|
||||||
(e) => e.type === "arrow",
|
(e) => e.type === "arrow",
|
||||||
) as ExcalidrawLinearElement[];
|
) as ExcalidrawLinearElement[];
|
||||||
|
|
||||||
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
||||||
clonedElements as any as typeof origElements;
|
duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
||||||
expect(
|
expect(
|
||||||
|
@ -327,10 +326,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const duplicatedElements = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
}).duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
clonedRectangle,
|
clonedRectangle,
|
||||||
|
@ -338,7 +337,7 @@ describe("duplicating multiple elements", () => {
|
||||||
clonedArrow1,
|
clonedArrow1,
|
||||||
clonedArrow2,
|
clonedArrow2,
|
||||||
clonedArrow3,
|
clonedArrow3,
|
||||||
] = clonedElements;
|
] = duplicatedElements;
|
||||||
|
|
||||||
expect(clonedRectangle.boundElements).toEqual([
|
expect(clonedRectangle.boundElements).toEqual([
|
||||||
{ id: clonedArrow1.id, type: "arrow" },
|
{ id: clonedArrow1.id, type: "arrow" },
|
||||||
|
@ -374,12 +373,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
});
|
||||||
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||||
clonedElements;
|
duplicatedElements;
|
||||||
|
|
||||||
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
||||||
|
@ -399,7 +398,7 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
newElements: [clonedRectangle1],
|
duplicatedElements: [clonedRectangle1],
|
||||||
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||||
|
|
||||||
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
||||||
|
@ -408,6 +407,117 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("group-related duplication", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("action-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(rectangle2.x + 50, rectangle2.y + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group away outside frame", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame, rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(h.elements);
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: frame.id },
|
||||||
|
{ id: rectangle1.id, frameId: frame.id },
|
||||||
|
{ id: rectangle2.id, frameId: frame.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: [], frameId: null },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplication z-order", () => {
|
describe("duplication z-order", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
|
@ -503,8 +613,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -538,8 +648,8 @@ describe("duplication z-order", () => {
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ id: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -569,8 +679,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -605,19 +715,19 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ [ORIG_ID]: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id, selected: true },
|
{ [ORIG_ID]: rectangle2.id, selected: true },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (in-order)", async () => {
|
it("alt-duplicating text container (in-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([rectangle, text]);
|
API.setElements([rectangle, text]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -625,20 +735,20 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (out-of-order)", async () => {
|
it("alt-duplicating text container (out-of-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([text, rectangle]);
|
API.setElements([text, rectangle]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -646,21 +756,21 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (in-order)", async () => {
|
it("alt-duplicating labeled arrows (in-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([arrow, text]);
|
API.setElements([arrow, text]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -668,21 +778,24 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
|
expect(h.state.selectedLinearElement).toEqual(
|
||||||
|
expect.objectContaining({ elementId: getCloneByOrigId(arrow.id)?.id }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (out-of-order)", async () => {
|
it("alt-duplicating labeled arrows (out-of-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([text, arrow]);
|
API.setElements([text, arrow]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -690,13 +803,50 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
]);
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating bindable element with bound arrow should keep the arrow on the duplicate", async () => {
|
||||||
|
const rect = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: -100,
|
||||||
|
y: 50,
|
||||||
|
width: 95,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(5, 5);
|
||||||
|
mouse.up(15, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{
|
||||||
|
id: rect.id,
|
||||||
|
boundElements: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: arrow.id }),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{ [ORIG_ID]: rect.id, boundElements: [], selected: true },
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
endBinding: expect.objectContaining({ elementId: rect.id }),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isArrowElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
|
@ -46,6 +47,8 @@ import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
import type { Radians } from "../../math/src";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
|
@ -155,6 +158,7 @@ export const actionBindText = register({
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
|
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
|
@ -226,8 +230,8 @@ export const actionWrapTextInContainer = register({
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
const someTextElements = selectedElements.some((el) => isTextElement(el));
|
||||||
return selectedElements.length > 0 && areTextElements;
|
return selectedElements.length > 0 && someTextElements;
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { ToolButton } from "../components/ToolButton";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import {
|
import {
|
||||||
handIcon,
|
handIcon,
|
||||||
|
LassoIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
@ -52,7 +53,6 @@ import type { AppState, Offsets } from "../types";
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
label: "labels.canvasBackground",
|
label: "labels.canvasBackground",
|
||||||
paletteName: "Change canvas background color",
|
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
predicate: (elements, appState, props, app) => {
|
predicate: (elements, appState, props, app) => {
|
||||||
return (
|
return (
|
||||||
|
@ -90,7 +90,6 @@ export const actionChangeViewBackgroundColor = register({
|
||||||
export const actionClearCanvas = register({
|
export const actionClearCanvas = register({
|
||||||
name: "clearCanvas",
|
name: "clearCanvas",
|
||||||
label: "labels.clearCanvas",
|
label: "labels.clearCanvas",
|
||||||
paletteName: "Clear canvas",
|
|
||||||
icon: TrashIcon,
|
icon: TrashIcon,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
predicate: (elements, appState, props, app) => {
|
predicate: (elements, appState, props, app) => {
|
||||||
|
@ -525,10 +524,42 @@ export const actionToggleEraserTool = register({
|
||||||
keyTest: (event) => event.key === KEYS.E,
|
keyTest: (event) => event.key === KEYS.E,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionToggleLassoTool = register({
|
||||||
|
name: "toggleLassoTool",
|
||||||
|
label: "toolBar.lasso",
|
||||||
|
icon: LassoIcon,
|
||||||
|
trackEvent: { category: "toolbar" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
|
if (appState.activeTool.type !== "lasso") {
|
||||||
|
activeTool = updateActiveTool(appState, {
|
||||||
|
type: "lasso",
|
||||||
|
fromSelection: false,
|
||||||
|
});
|
||||||
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
|
||||||
|
} else {
|
||||||
|
activeTool = updateActiveTool(appState, {
|
||||||
|
type: "selection",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
|
activeTool,
|
||||||
|
},
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const actionToggleHandTool = register({
|
export const actionToggleHandTool = register({
|
||||||
name: "toggleHandTool",
|
name: "toggleHandTool",
|
||||||
label: "toolBar.hand",
|
label: "toolBar.hand",
|
||||||
paletteName: "Toggle hand tool",
|
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
icon: handIcon,
|
icon: handIcon,
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
|
|
|
@ -7,26 +7,17 @@ import {
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isLinearElement,
|
|
||||||
} from "@excalidraw/element/typeChecks";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
|
getSelectionStateForElements,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element/selection";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
|
@ -65,8 +56,7 @@ export const actionDuplicateSelection = register({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||||
duplicateElements({
|
|
||||||
type: "in-place",
|
type: "in-place",
|
||||||
elements,
|
elements,
|
||||||
idsOfElementsToDuplicate: arrayToMap(
|
idsOfElementsToDuplicate: arrayToMap(
|
||||||
|
@ -77,40 +67,38 @@ export const actionDuplicateSelection = register({
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
overrides: (element) => ({
|
overrides: ({ origElement, origIdToDuplicateId }) => {
|
||||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
const duplicateFrameId =
|
||||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
|
||||||
}),
|
return {
|
||||||
reverseOrder: false,
|
x: origElement.x + DEFAULT_GRID_SIZE / 2,
|
||||||
|
y: origElement.y + DEFAULT_GRID_SIZE / 2,
|
||||||
|
frameId: duplicateFrameId ?? origElement.frameId,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app.props.onDuplicate && nextElements) {
|
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
const mappedElements = app.props.onDuplicate(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
elements,
|
||||||
|
);
|
||||||
if (mappedElements) {
|
if (mappedElements) {
|
||||||
nextElements = mappedElements;
|
elementsWithDuplicates = mappedElements;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
elements: syncMovedIndices(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
arrayToMap(duplicatedElements),
|
||||||
|
),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...updateLinearElementEditors(duplicatedElements),
|
...getSelectionStateForElements(
|
||||||
...selectGroupsForSelectedElements(
|
|
||||||
{
|
|
||||||
editingGroupId: appState.editingGroupId,
|
|
||||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
|
||||||
duplicatedElements,
|
duplicatedElements,
|
||||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
getNonDeletedElements(elementsWithDuplicates),
|
||||||
if (!isBoundToContainer(element)) {
|
|
||||||
acc[element.id] = true;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
getNonDeletedElements(nextElements),
|
|
||||||
appState,
|
appState,
|
||||||
null,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
@ -130,24 +118,3 @@ export const actionDuplicateSelection = register({
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
|
|
||||||
const linears = clonedElements.filter(isLinearElement);
|
|
||||||
if (linears.length === 1) {
|
|
||||||
const linear = linears[0];
|
|
||||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
|
||||||
const onlySingleLinearSelected = clonedElements.every(
|
|
||||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (onlySingleLinearSelected) {
|
|
||||||
return {
|
|
||||||
selectedLinearElement: new LinearElementEditor(linear),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedLinearElement: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -90,7 +90,6 @@ export const actionToggleElementLock = register({
|
||||||
|
|
||||||
export const actionUnlockAllElements = register({
|
export const actionUnlockAllElements = register({
|
||||||
name: "unlockAllElements",
|
name: "unlockAllElements",
|
||||||
paletteName: "Unlock all elements",
|
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
icon: UnlockedIcon,
|
icon: UnlockedIcon,
|
||||||
|
|
|
@ -9,7 +9,6 @@ export const actionToggleStats = register({
|
||||||
name: "stats",
|
name: "stats",
|
||||||
label: "stats.fullTitle",
|
label: "stats.fullTitle",
|
||||||
icon: abacusIcon,
|
icon: abacusIcon,
|
||||||
paletteName: "Toggle stats",
|
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "menu" },
|
trackEvent: { category: "menu" },
|
||||||
keywords: ["edit", "attributes", "customize"],
|
keywords: ["edit", "attributes", "customize"],
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { register } from "./register";
|
||||||
export const actionToggleViewMode = register({
|
export const actionToggleViewMode = register({
|
||||||
name: "viewMode",
|
name: "viewMode",
|
||||||
label: "labels.viewMode",
|
label: "labels.viewMode",
|
||||||
paletteName: "Toggle view mode",
|
|
||||||
icon: eyeIcon,
|
icon: eyeIcon,
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: {
|
trackEvent: {
|
||||||
|
|
|
@ -9,7 +9,6 @@ export const actionToggleZenMode = register({
|
||||||
name: "zenMode",
|
name: "zenMode",
|
||||||
label: "buttons.zenMode",
|
label: "buttons.zenMode",
|
||||||
icon: coffeeIcon,
|
icon: coffeeIcon,
|
||||||
paletteName: "Toggle zen mode",
|
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "canvas",
|
category: "canvas",
|
||||||
|
|
|
@ -140,6 +140,7 @@ export type ActionName =
|
||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor"
|
| "cropEditor"
|
||||||
| "wrapSelectionInFrame"
|
| "wrapSelectionInFrame"
|
||||||
|
| "toggleLassoTool"
|
||||||
| "toggleShapeSwitch";
|
| "toggleShapeSwitch";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
|
|
|
@ -23,6 +23,8 @@ export interface Trail {
|
||||||
|
|
||||||
export interface AnimatedTrailOptions {
|
export interface AnimatedTrailOptions {
|
||||||
fill: (trail: AnimatedTrail) => string;
|
fill: (trail: AnimatedTrail) => string;
|
||||||
|
stroke?: (trail: AnimatedTrail) => string;
|
||||||
|
animateTrail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AnimatedTrail implements Trail {
|
export class AnimatedTrail implements Trail {
|
||||||
|
@ -31,16 +33,28 @@ export class AnimatedTrail implements Trail {
|
||||||
|
|
||||||
private container?: SVGSVGElement;
|
private container?: SVGSVGElement;
|
||||||
private trailElement: SVGPathElement;
|
private trailElement: SVGPathElement;
|
||||||
|
private trailAnimation?: SVGAnimateElement;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private animationFrameHandler: AnimationFrameHandler,
|
private animationFrameHandler: AnimationFrameHandler,
|
||||||
private app: App,
|
protected app: App,
|
||||||
private options: Partial<LaserPointerOptions> &
|
private options: Partial<LaserPointerOptions> &
|
||||||
Partial<AnimatedTrailOptions>,
|
Partial<AnimatedTrailOptions>,
|
||||||
) {
|
) {
|
||||||
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
||||||
|
|
||||||
this.trailElement = document.createElementNS(SVG_NS, "path");
|
this.trailElement = document.createElementNS(SVG_NS, "path");
|
||||||
|
if (this.options.animateTrail) {
|
||||||
|
this.trailAnimation = document.createElementNS(SVG_NS, "animate");
|
||||||
|
// TODO: make this configurable
|
||||||
|
this.trailAnimation.setAttribute("attributeName", "stroke-dashoffset");
|
||||||
|
this.trailElement.setAttribute("stroke-dasharray", "7 7");
|
||||||
|
this.trailElement.setAttribute("stroke-dashoffset", "10");
|
||||||
|
this.trailAnimation.setAttribute("from", "0");
|
||||||
|
this.trailAnimation.setAttribute("to", `-14`);
|
||||||
|
this.trailAnimation.setAttribute("dur", "0.3s");
|
||||||
|
this.trailElement.appendChild(this.trailAnimation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasCurrentTrail() {
|
get hasCurrentTrail() {
|
||||||
|
@ -104,8 +118,23 @@ export class AnimatedTrail implements Trail {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCurrentTrail() {
|
||||||
|
return this.currentTrail;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTrails() {
|
||||||
|
this.pastTrails = [];
|
||||||
|
this.currentTrail = undefined;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
this.pastTrails = [];
|
||||||
this.start();
|
this.start();
|
||||||
|
if (this.trailAnimation) {
|
||||||
|
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||||
|
this.trailAnimation.setAttribute("repeatCount", "indefinite");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onFrame() {
|
private onFrame() {
|
||||||
|
@ -132,14 +161,25 @@ export class AnimatedTrail implements Trail {
|
||||||
const svgPaths = paths.join(" ").trim();
|
const svgPaths = paths.join(" ").trim();
|
||||||
|
|
||||||
this.trailElement.setAttribute("d", svgPaths);
|
this.trailElement.setAttribute("d", svgPaths);
|
||||||
|
if (this.trailAnimation) {
|
||||||
|
this.trailElement.setAttribute(
|
||||||
|
"fill",
|
||||||
|
(this.options.fill ?? (() => "black"))(this),
|
||||||
|
);
|
||||||
|
this.trailElement.setAttribute(
|
||||||
|
"stroke",
|
||||||
|
(this.options.stroke ?? (() => "black"))(this),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.trailElement.setAttribute(
|
this.trailElement.setAttribute(
|
||||||
"fill",
|
"fill",
|
||||||
(this.options.fill ?? (() => "black"))(this),
|
(this.options.fill ?? (() => "black"))(this),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private drawTrail(trail: LaserPointer, state: AppState): string {
|
private drawTrail(trail: LaserPointer, state: AppState): string {
|
||||||
const stroke = trail
|
const _stroke = trail
|
||||||
.getStrokeOutline(trail.options.size / state.zoom.value)
|
.getStrokeOutline(trail.options.size / state.zoom.value)
|
||||||
.map(([x, y]) => {
|
.map(([x, y]) => {
|
||||||
const result = sceneCoordsToViewportCoords(
|
const result = sceneCoordsToViewportCoords(
|
||||||
|
@ -150,6 +190,10 @@ export class AnimatedTrail implements Trail {
|
||||||
return [result.x, result.y];
|
return [result.x, result.y];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stroke = this.trailAnimation
|
||||||
|
? _stroke.slice(0, _stroke.length / 2)
|
||||||
|
: _stroke;
|
||||||
|
|
||||||
return getSvgPathFromStroke(stroke, true);
|
return getSvgPathFromStroke(stroke, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
type: "selection",
|
type: "selection",
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: DEFAULT_ELEMENT_PROPS.locked,
|
locked: DEFAULT_ELEMENT_PROPS.locked,
|
||||||
|
fromSelection: false,
|
||||||
lastActiveTool: null,
|
lastActiveTool: null,
|
||||||
},
|
},
|
||||||
penMode: false,
|
penMode: false,
|
||||||
|
|
|
@ -62,6 +62,7 @@ import {
|
||||||
mermaidLogoIcon,
|
mermaidLogoIcon,
|
||||||
laserPointerToolIcon,
|
laserPointerToolIcon,
|
||||||
MagicIcon,
|
MagicIcon,
|
||||||
|
LassoIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
|
||||||
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||||
|
@ -83,7 +84,6 @@ export const canChangeStrokeColor = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(hasStrokeColor(appState.activeTool.type) &&
|
(hasStrokeColor(appState.activeTool.type) &&
|
||||||
appState.activeTool.type !== "image" &&
|
|
||||||
commonSelectedType !== "image" &&
|
commonSelectedType !== "image" &&
|
||||||
commonSelectedType !== "frame" &&
|
commonSelectedType !== "frame" &&
|
||||||
commonSelectedType !== "magicframe") ||
|
commonSelectedType !== "magicframe") ||
|
||||||
|
@ -295,6 +295,8 @@ export const ShapesSwitcher = ({
|
||||||
|
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
|
const lassoToolSelected = activeTool.type === "lasso";
|
||||||
|
|
||||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||||
|
|
||||||
const { TTDDialogTriggerTunnel } = useTunnels();
|
const { TTDDialogTriggerTunnel } = useTunnels();
|
||||||
|
@ -316,6 +318,7 @@ export const ShapesSwitcher = ({
|
||||||
const shortcut = letter
|
const shortcut = letter
|
||||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||||
: `${numericKey}`;
|
: `${numericKey}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable })}
|
className={clsx("Shape", { fillable })}
|
||||||
|
@ -333,6 +336,14 @@ export const ShapesSwitcher = ({
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
app.togglePenMode(true);
|
app.togglePenMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value === "selection") {
|
||||||
|
if (appState.activeTool.type === "selection") {
|
||||||
|
app.setActiveTool({ type: "lasso" });
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: "selection" });
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
if (appState.activeTool.type !== value) {
|
if (appState.activeTool.type !== value) {
|
||||||
|
@ -358,6 +369,7 @@ export const ShapesSwitcher = ({
|
||||||
"App-toolbar__extra-tools-trigger--selected":
|
"App-toolbar__extra-tools-trigger--selected":
|
||||||
frameToolSelected ||
|
frameToolSelected ||
|
||||||
embeddableToolSelected ||
|
embeddableToolSelected ||
|
||||||
|
lassoToolSelected ||
|
||||||
// in collab we're already highlighting the laser button
|
// in collab we're already highlighting the laser button
|
||||||
// outside toolbar, so let's not highlight extra-tools button
|
// outside toolbar, so let's not highlight extra-tools button
|
||||||
// on top of it
|
// on top of it
|
||||||
|
@ -366,7 +378,15 @@ export const ShapesSwitcher = ({
|
||||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
title={t("toolBar.extraTools")}
|
title={t("toolBar.extraTools")}
|
||||||
>
|
>
|
||||||
{extraToolsIcon}
|
{frameToolSelected
|
||||||
|
? frameToolIcon
|
||||||
|
: embeddableToolSelected
|
||||||
|
? EmbedIcon
|
||||||
|
: laserToolSelected && !app.props.isCollaborating
|
||||||
|
? laserPointerToolIcon
|
||||||
|
: lassoToolSelected
|
||||||
|
? LassoIcon
|
||||||
|
: extraToolsIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
|
@ -399,6 +419,14 @@ export const ShapesSwitcher = ({
|
||||||
>
|
>
|
||||||
{t("toolBar.laser")}
|
{t("toolBar.laser")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||||
|
icon={LassoIcon}
|
||||||
|
data-testid="toolbar-lasso"
|
||||||
|
selected={lassoToolSelected}
|
||||||
|
>
|
||||||
|
{t("toolBar.lasso")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||||
Generate
|
Generate
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -99,6 +99,7 @@ import {
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
type EXPORT_IMAGE_TYPES,
|
type EXPORT_IMAGE_TYPES,
|
||||||
|
randomInteger,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -278,6 +279,7 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
excludeElementsInFramesFromSelection,
|
||||||
|
getSelectionStateForElements,
|
||||||
makeNextSelectedElementIds,
|
makeNextSelectedElementIds,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element/selection";
|
||||||
|
|
||||||
|
@ -453,7 +455,6 @@ import {
|
||||||
import { Emitter } from "../emitter";
|
import { Emitter } from "../emitter";
|
||||||
import { ElementCanvasButtons } from "../components/ElementCanvasButtons";
|
import { ElementCanvasButtons } from "../components/ElementCanvasButtons";
|
||||||
import { Store, CaptureUpdateAction } from "../store";
|
import { Store, CaptureUpdateAction } from "../store";
|
||||||
import { AnimatedTrail } from "../animated-trail";
|
|
||||||
import { LaserTrails } from "../laser-trails";
|
import { LaserTrails } from "../laser-trails";
|
||||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||||
import { textWysiwyg } from "../wysiwyg/textWysiwyg";
|
import { textWysiwyg } from "../wysiwyg/textWysiwyg";
|
||||||
|
@ -461,6 +462,10 @@ import { isOverScrollBars } from "../scene/scrollbars";
|
||||||
|
|
||||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||||
|
|
||||||
|
import { LassoTrail } from "../lasso";
|
||||||
|
|
||||||
|
import { EraserTrail } from "../eraser";
|
||||||
|
|
||||||
import ShapeSwitch, {
|
import ShapeSwitch, {
|
||||||
getSwitchableTypeFromElements,
|
getSwitchableTypeFromElements,
|
||||||
shapeSwitchAtom,
|
shapeSwitchAtom,
|
||||||
|
@ -677,26 +682,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
animationFrameHandler = new AnimationFrameHandler();
|
animationFrameHandler = new AnimationFrameHandler();
|
||||||
|
|
||||||
laserTrails = new LaserTrails(this.animationFrameHandler, this);
|
laserTrails = new LaserTrails(this.animationFrameHandler, this);
|
||||||
eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, {
|
eraserTrail = new EraserTrail(this.animationFrameHandler, this);
|
||||||
streamline: 0.2,
|
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
|
||||||
size: 5,
|
|
||||||
keepHead: true,
|
|
||||||
sizeMapping: (c) => {
|
|
||||||
const DECAY_TIME = 200;
|
|
||||||
const DECAY_LENGTH = 10;
|
|
||||||
const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME);
|
|
||||||
const l =
|
|
||||||
(DECAY_LENGTH -
|
|
||||||
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
|
|
||||||
DECAY_LENGTH;
|
|
||||||
|
|
||||||
return Math.min(easeOut(l), easeOut(t));
|
|
||||||
},
|
|
||||||
fill: () =>
|
|
||||||
this.state.theme === THEME.LIGHT
|
|
||||||
? "rgba(0, 0, 0, 0.2)"
|
|
||||||
: "rgba(255, 255, 255, 0.2)",
|
|
||||||
});
|
|
||||||
|
|
||||||
onChangeEmitter = new Emitter<
|
onChangeEmitter = new Emitter<
|
||||||
[
|
[
|
||||||
|
@ -1675,7 +1662,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
<SVGLayer
|
<SVGLayer
|
||||||
trails={[this.laserTrails, this.eraserTrail]}
|
trails={[
|
||||||
|
this.laserTrails,
|
||||||
|
this.lassoTrail,
|
||||||
|
this.eraserTrail,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
{selectedElements.length === 1 &&
|
{selectedElements.length === 1 &&
|
||||||
this.state.openDialog?.name !==
|
this.state.openDialog?.name !==
|
||||||
|
@ -1844,6 +1835,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
scale={window.devicePixelRatio}
|
scale={window.devicePixelRatio}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
|
renderScrollbars={
|
||||||
|
this.props.renderScrollbars === true
|
||||||
|
}
|
||||||
device={this.device}
|
device={this.device}
|
||||||
renderInteractiveSceneCallback={
|
renderInteractiveSceneCallback={
|
||||||
this.renderInteractiveSceneCallback
|
this.renderInteractiveSceneCallback
|
||||||
|
@ -3283,7 +3277,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||||
|
|
||||||
const { newElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
|
@ -3295,7 +3289,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||||
let nextElements = [...prevElements, ...newElements];
|
let nextElements = [...prevElements, ...duplicatedElements];
|
||||||
|
|
||||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||||
nextElements,
|
nextElements,
|
||||||
|
@ -3304,13 +3298,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
nextElements = mappedNewSceneElements || nextElements;
|
nextElements = mappedNewSceneElements || nextElements;
|
||||||
|
|
||||||
syncMovedIndices(nextElements, arrayToMap(newElements));
|
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
|
||||||
|
|
||||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||||
|
|
||||||
if (topLayerFrame) {
|
if (topLayerFrame) {
|
||||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||||
newElements,
|
duplicatedElements,
|
||||||
topLayerFrame,
|
topLayerFrame,
|
||||||
);
|
);
|
||||||
addElementsToFrame(
|
addElementsToFrame(
|
||||||
|
@ -3323,7 +3317,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
|
|
||||||
newElements.forEach((newElement) => {
|
duplicatedElements.forEach((newElement) => {
|
||||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||||
const container = getContainerElement(
|
const container = getContainerElement(
|
||||||
newElement,
|
newElement,
|
||||||
|
@ -3339,7 +3333,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
Fonts.loadElementsFonts(newElements).then((fontFaces) => {
|
Fonts.loadElementsFonts(duplicatedElements).then((fontFaces) => {
|
||||||
this.fonts.onLoaded(fontFaces);
|
this.fonts.onLoaded(fontFaces);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3351,7 +3345,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
|
|
||||||
const nextElementsToSelect =
|
const nextElementsToSelect =
|
||||||
excludeElementsInFramesFromSelection(newElements);
|
excludeElementsInFramesFromSelection(duplicatedElements);
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
|
@ -3394,7 +3388,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: "selection" });
|
||||||
|
|
||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(newElements, {
|
this.scrollToContent(duplicatedElements, {
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
canvasOffsets: this.getEditorUIOffsets(),
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
|
@ -4666,7 +4660,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.openDialog?.name === "elementLinkSelector"
|
this.state.openDialog?.name === "elementLinkSelector"
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (this.state.activeTool.type === "selection") {
|
} else if (
|
||||||
|
this.state.activeTool.type === "selection" ||
|
||||||
|
this.state.activeTool.type === "lasso"
|
||||||
|
) {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.interactiveCanvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
|
@ -4774,7 +4771,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean },
|
) & { locked?: boolean; fromSelection?: boolean },
|
||||||
|
keepSelection = false,
|
||||||
) => {
|
) => {
|
||||||
if (!this.isToolSupported(tool.type)) {
|
if (!this.isToolSupported(tool.type)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
@ -4816,7 +4814,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextActiveTool.type !== "selection") {
|
if (nextActiveTool.type === "lasso") {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
...(keepSelection
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||||
|
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
|
||||||
|
editingGroupId: null,
|
||||||
|
multiElement: null,
|
||||||
|
}),
|
||||||
|
...commonResets,
|
||||||
|
};
|
||||||
|
} else if (nextActiveTool.type !== "selection") {
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
|
@ -5173,7 +5185,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getElementHitThreshold() {
|
getElementHitThreshold() {
|
||||||
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5362,15 +5374,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
y: sceneY,
|
y: sceneY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const element = existingTextElement
|
const element =
|
||||||
? existingTextElement
|
existingTextElement ||
|
||||||
: newTextElement({
|
newTextElement({
|
||||||
x: parentCenterPosition
|
x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX,
|
||||||
? parentCenterPosition.elementCenterX
|
y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY,
|
||||||
: sceneX,
|
|
||||||
y: parentCenterPosition
|
|
||||||
? parentCenterPosition.elementCenterY
|
|
||||||
: sceneY,
|
|
||||||
strokeColor: this.state.currentItemStrokeColor,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
backgroundColor: this.state.currentItemBackgroundColor,
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
fillStyle: this.state.currentItemFillStyle,
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
|
@ -5390,7 +5398,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
lineHeight,
|
lineHeight,
|
||||||
angle: container?.angle ?? (0 as Radians),
|
angle: container
|
||||||
|
? isArrowElement(container)
|
||||||
|
? (0 as Radians)
|
||||||
|
: container.angle
|
||||||
|
: (0 as Radians),
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6229,101 +6241,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
private handleEraser = (
|
private handleEraser = (
|
||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
pointerDownState: PointerDownState,
|
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
) => {
|
) => {
|
||||||
this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y);
|
const elementsToErase = this.eraserTrail.addPointToPath(
|
||||||
|
scenePointer.x,
|
||||||
let didChange = false;
|
scenePointer.y,
|
||||||
|
event.altKey,
|
||||||
const processedGroups = new Set<ExcalidrawElement["id"]>();
|
|
||||||
const nonDeletedElements = this.scene.getNonDeletedElements();
|
|
||||||
|
|
||||||
const processElements = (elements: ExcalidrawElement[]) => {
|
|
||||||
for (const element of elements) {
|
|
||||||
if (element.locked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.altKey) {
|
|
||||||
if (this.elementsPendingErasure.delete(element.id)) {
|
|
||||||
didChange = true;
|
|
||||||
}
|
|
||||||
} else if (!this.elementsPendingErasure.has(element.id)) {
|
|
||||||
didChange = true;
|
|
||||||
this.elementsPendingErasure.add(element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// (un)erase groups atomically
|
|
||||||
if (didChange && element.groupIds?.length) {
|
|
||||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
|
||||||
if (!processedGroups.has(shallowestGroupId)) {
|
|
||||||
processedGroups.add(shallowestGroupId);
|
|
||||||
const elems = getElementsInGroup(
|
|
||||||
nonDeletedElements,
|
|
||||||
shallowestGroupId,
|
|
||||||
);
|
);
|
||||||
for (const elem of elems) {
|
|
||||||
if (event.altKey) {
|
|
||||||
this.elementsPendingErasure.delete(elem.id);
|
|
||||||
} else {
|
|
||||||
this.elementsPendingErasure.add(elem.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const distance = pointDistance(
|
this.elementsPendingErasure = new Set(elementsToErase);
|
||||||
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
|
||||||
);
|
|
||||||
const threshold = this.getElementHitThreshold();
|
|
||||||
const p = { ...pointerDownState.lastCoords };
|
|
||||||
let samplingInterval = 0;
|
|
||||||
while (samplingInterval <= distance) {
|
|
||||||
const hitElements = this.getElementsAtPosition(p.x, p.y);
|
|
||||||
processElements(hitElements);
|
|
||||||
|
|
||||||
// Exit since we reached current point
|
|
||||||
if (samplingInterval === distance) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate next point in the line at a distance of sampling interval
|
|
||||||
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
|
||||||
|
|
||||||
const distanceRatio = samplingInterval / distance;
|
|
||||||
const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x;
|
|
||||||
const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y;
|
|
||||||
p.x = nextX;
|
|
||||||
p.y = nextY;
|
|
||||||
}
|
|
||||||
|
|
||||||
pointerDownState.lastCoords.x = scenePointer.x;
|
|
||||||
pointerDownState.lastCoords.y = scenePointer.y;
|
|
||||||
|
|
||||||
if (didChange) {
|
|
||||||
for (const element of this.scene.getNonDeletedElements()) {
|
|
||||||
if (
|
|
||||||
isBoundToContainer(element) &&
|
|
||||||
(this.elementsPendingErasure.has(element.id) ||
|
|
||||||
this.elementsPendingErasure.has(element.containerId))
|
|
||||||
) {
|
|
||||||
if (event.altKey) {
|
|
||||||
this.elementsPendingErasure.delete(element.id);
|
|
||||||
this.elementsPendingErasure.delete(element.containerId);
|
|
||||||
} else {
|
|
||||||
this.elementsPendingErasure.add(element.id);
|
|
||||||
this.elementsPendingErasure.add(element.containerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
|
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// set touch moving for mobile context menu
|
// set touch moving for mobile context menu
|
||||||
|
@ -6643,6 +6570,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
!this.state.penMode ||
|
!this.state.penMode ||
|
||||||
event.pointerType !== "touch" ||
|
event.pointerType !== "touch" ||
|
||||||
this.state.activeTool.type === "selection" ||
|
this.state.activeTool.type === "selection" ||
|
||||||
|
this.state.activeTool.type === "lasso" ||
|
||||||
this.state.activeTool.type === "text" ||
|
this.state.activeTool.type === "text" ||
|
||||||
this.state.activeTool.type === "image";
|
this.state.activeTool.type === "image";
|
||||||
|
|
||||||
|
@ -6650,7 +6578,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.activeTool.type === "text") {
|
if (this.state.activeTool.type === "lasso") {
|
||||||
|
this.lassoTrail.startPath(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
} else if (this.state.activeTool.type === "text") {
|
||||||
this.handleTextOnPointerDown(event, pointerDownState);
|
this.handleTextOnPointerDown(event, pointerDownState);
|
||||||
} else if (
|
} else if (
|
||||||
this.state.activeTool.type === "arrow" ||
|
this.state.activeTool.type === "arrow" ||
|
||||||
|
@ -7052,6 +6986,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
drag: {
|
drag: {
|
||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
offset: null,
|
offset: null,
|
||||||
|
origin: { ...origin },
|
||||||
},
|
},
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
onMove: null,
|
onMove: null,
|
||||||
|
@ -7107,7 +7042,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearSelectionIfNotUsingSelection = (): void => {
|
private clearSelectionIfNotUsingSelection = (): void => {
|
||||||
if (this.state.activeTool.type !== "selection") {
|
if (
|
||||||
|
this.state.activeTool.type !== "selection" &&
|
||||||
|
this.state.activeTool.type !== "lasso"
|
||||||
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
@ -8163,7 +8101,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEraserActive(this.state)) {
|
if (isEraserActive(this.state)) {
|
||||||
this.handleEraser(event, pointerDownState, pointerCoords);
|
this.handleEraser(event, pointerCoords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8307,7 +8245,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (
|
if (
|
||||||
(hasHitASelectedElement ||
|
(hasHitASelectedElement ||
|
||||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||||
!isSelectingPointsInLineEditor
|
!isSelectingPointsInLineEditor &&
|
||||||
|
this.state.activeTool.type !== "lasso"
|
||||||
) {
|
) {
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
|
|
||||||
|
@ -8342,8 +8281,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.activeEmbeddable?.state !== "active"
|
this.state.activeEmbeddable?.state !== "active"
|
||||||
) {
|
) {
|
||||||
const dragOffset = {
|
const dragOffset = {
|
||||||
x: pointerCoords.x - pointerDownState.origin.x,
|
x: pointerCoords.x - pointerDownState.drag.origin.x,
|
||||||
y: pointerCoords.y - pointerDownState.origin.y,
|
y: pointerCoords.y - pointerDownState.drag.origin.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalElements = [
|
const originalElements = [
|
||||||
|
@ -8525,51 +8464,125 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
|
// hit element may not end up being selected
|
||||||
|
// if we're alt-dragging a common bounding box
|
||||||
|
// over the hit element
|
||||||
|
pointerDownState.hit.wasAddedToSelection &&
|
||||||
!selectedElements.find((el) => el.id === hitElement.id)
|
!selectedElements.find((el) => el.id === hitElement.id)
|
||||||
) {
|
) {
|
||||||
selectedElements.push(hitElement);
|
selectedElements.push(hitElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newElements: clonedElements, elementsWithClones } =
|
const idsOfElementsToDuplicate = new Map(
|
||||||
duplicateElements({
|
selectedElements.map((el) => [el.id, el]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
duplicatedElements,
|
||||||
|
duplicateElementsMap,
|
||||||
|
elementsWithDuplicates,
|
||||||
|
origIdToDuplicateId,
|
||||||
|
} = duplicateElements({
|
||||||
type: "in-place",
|
type: "in-place",
|
||||||
elements,
|
elements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
idsOfElementsToDuplicate: new Map(
|
idsOfElementsToDuplicate,
|
||||||
selectedElements.map((el) => [el.id, el]),
|
overrides: ({ duplicateElement, origElement }) => {
|
||||||
),
|
return {
|
||||||
overrides: (el) => {
|
// reset to the original element's frameId (unless we've
|
||||||
|
// duplicated alongside a frame in which case we need to
|
||||||
|
// keep the duplicate frame's id) so that the element
|
||||||
|
// frame membership is refreshed on pointerup
|
||||||
|
// NOTE this is a hacky solution and should be done
|
||||||
|
// differently
|
||||||
|
frameId: duplicateElement.frameId ?? origElement.frameId,
|
||||||
|
seed: randomInteger(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
duplicatedElements.forEach((element) => {
|
||||||
|
pointerDownState.originalElements.set(
|
||||||
|
element.id,
|
||||||
|
deepCopyElement(element),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedClonedElements = elementsWithDuplicates.map((el) => {
|
||||||
|
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||||
const origEl = pointerDownState.originalElements.get(el.id);
|
const origEl = pointerDownState.originalElements.get(el.id);
|
||||||
|
|
||||||
if (origEl) {
|
if (origEl) {
|
||||||
return {
|
return newElementWith(el, {
|
||||||
x: origEl.x,
|
x: origEl.x,
|
||||||
y: origEl.y,
|
y: origEl.y,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
reverseOrder: true,
|
|
||||||
});
|
});
|
||||||
clonedElements.forEach((element) => {
|
}
|
||||||
pointerDownState.originalElements.set(element.id, element);
|
}
|
||||||
|
return el;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||||
elementsWithClones,
|
mappedClonedElements,
|
||||||
elements,
|
elements,
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextSceneElements = syncMovedIndices(
|
const elementsWithIndices = syncMovedIndices(
|
||||||
mappedNewSceneElements || elementsWithClones,
|
mappedNewSceneElements || mappedClonedElements,
|
||||||
arrayToMap(clonedElements),
|
arrayToMap(duplicatedElements),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextSceneElements);
|
// we need to update synchronously so as to keep pointerDownState,
|
||||||
|
// appState, and scene elements in sync
|
||||||
|
flushSync(() => {
|
||||||
|
// swap hit element with the duplicated one
|
||||||
|
if (pointerDownState.hit.element) {
|
||||||
|
const cloneId = origIdToDuplicateId.get(
|
||||||
|
pointerDownState.hit.element.id,
|
||||||
|
);
|
||||||
|
const clonedElement =
|
||||||
|
cloneId && duplicateElementsMap.get(cloneId);
|
||||||
|
pointerDownState.hit.element = clonedElement || null;
|
||||||
|
}
|
||||||
|
// swap hit elements with the duplicated ones
|
||||||
|
pointerDownState.hit.allHitElements =
|
||||||
|
pointerDownState.hit.allHitElements.reduce(
|
||||||
|
(
|
||||||
|
acc: typeof pointerDownState.hit.allHitElements,
|
||||||
|
origHitElement,
|
||||||
|
) => {
|
||||||
|
const cloneId = origIdToDuplicateId.get(origHitElement.id);
|
||||||
|
const clonedElement =
|
||||||
|
cloneId && duplicateElementsMap.get(cloneId);
|
||||||
|
if (clonedElement) {
|
||||||
|
acc.push(clonedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// update drag origin to the position at which we started
|
||||||
|
// the duplication so that the drag offset is correct
|
||||||
|
pointerDownState.drag.origin = viewportCoordsToSceneCoords(
|
||||||
|
event,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
// switch selected elements to the duplicated ones
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
...getSelectionStateForElements(
|
||||||
|
duplicatedElements,
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.scene.replaceAllElements(elementsWithIndices);
|
||||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -8579,7 +8592,37 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (this.state.selectionElement) {
|
if (this.state.selectionElement) {
|
||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
|
if (event.altKey) {
|
||||||
|
this.setActiveTool(
|
||||||
|
{ type: "lasso", fromSelection: true },
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
this.lassoTrail.startPath(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
this.setAppState({
|
||||||
|
selectionElement: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
|
}
|
||||||
|
} else if (this.state.activeTool.type === "lasso") {
|
||||||
|
if (!event.altKey && this.state.activeTool.fromSelection) {
|
||||||
|
this.setActiveTool({ type: "selection" });
|
||||||
|
this.createGenericElementOnPointerDown("selection", pointerDownState);
|
||||||
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
|
this.lassoTrail.endPath();
|
||||||
|
} else {
|
||||||
|
this.lassoTrail.addPointToPath(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// It is very important to read this.state within each move event,
|
// It is very important to read this.state within each move event,
|
||||||
// otherwise we would read a stale one!
|
// otherwise we would read a stale one!
|
||||||
|
@ -8784,7 +8827,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const dx = x - pointerDownState.lastCoords.x;
|
const dx = x - pointerDownState.lastCoords.x;
|
||||||
this.translateCanvas({
|
this.translateCanvas({
|
||||||
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
scrollX:
|
||||||
|
this.state.scrollX -
|
||||||
|
(dx * (currentScrollBars.horizontal?.deltaMultiplier || 1)) /
|
||||||
|
this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.x = x;
|
pointerDownState.lastCoords.x = x;
|
||||||
return true;
|
return true;
|
||||||
|
@ -8794,7 +8840,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const dy = y - pointerDownState.lastCoords.y;
|
const dy = y - pointerDownState.lastCoords.y;
|
||||||
this.translateCanvas({
|
this.translateCanvas({
|
||||||
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
scrollY:
|
||||||
|
this.state.scrollY -
|
||||||
|
(dy * (currentScrollBars.vertical?.deltaMultiplier || 1)) /
|
||||||
|
this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.y = y;
|
pointerDownState.lastCoords.y = y;
|
||||||
return true;
|
return true;
|
||||||
|
@ -8834,6 +8883,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
originSnapOffset: null,
|
originSnapOffset: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// just in case, tool changes mid drag, always clean up
|
||||||
|
this.lassoTrail.endPath();
|
||||||
this.lastPointerMoveCoords = null;
|
this.lastPointerMoveCoords = null;
|
||||||
|
|
||||||
SnapCache.setReferenceSnapPoints(null);
|
SnapCache.setReferenceSnapPoints(null);
|
||||||
|
@ -9550,6 +9601,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
// do not clear selection if lasso is active
|
||||||
|
this.state.activeTool.type !== "lasso" &&
|
||||||
// not elbow midpoint dragged
|
// not elbow midpoint dragged
|
||||||
!(hitElement && isElbowArrow(hitElement)) &&
|
!(hitElement && isElbowArrow(hitElement)) &&
|
||||||
// not dragged
|
// not dragged
|
||||||
|
@ -9648,7 +9701,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
if (
|
||||||
|
!activeTool.locked &&
|
||||||
|
activeTool.type !== "freedraw" &&
|
||||||
|
(activeTool.type !== "lasso" ||
|
||||||
|
// if lasso is turned on but from selection => reset to selection
|
||||||
|
(activeTool.type === "lasso" && activeTool.fromSelection))
|
||||||
|
) {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState({
|
this.setState({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
|
@ -10503,7 +10562,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
||||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
shouldResizeFromCenter: false,
|
||||||
zoom: this.state.zoom.value,
|
zoom: this.state.zoom.value,
|
||||||
informMutation,
|
informMutation,
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
.color-picker-container {
|
.color-picker-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 8px 1.625rem;
|
grid-template-columns: 1fr 20px 1.625rem;
|
||||||
padding: 0.25rem 0px;
|
padding: 0.25rem 0px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -27,14 +27,19 @@
|
||||||
.color-picker__top-picks {
|
.color-picker__top-picks {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker__button {
|
.color-picker__button {
|
||||||
--radius: 6px;
|
--radius: 4px;
|
||||||
--size: 1.375rem;
|
--size: 1.375rem;
|
||||||
|
|
||||||
|
&.has-outline {
|
||||||
|
box-shadow: inset 0 0 0 1px #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 1px;
|
margin: 0;
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -46,15 +51,19 @@
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:hover:not(.active) {
|
&:hover:not(.active):not(.color-picker__button--large) {
|
||||||
|
transform: scale(1.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active).color-picker__button--large {
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -1px;
|
||||||
left: 0;
|
left: -1px;
|
||||||
right: 0;
|
right: -1px;
|
||||||
bottom: 0;
|
bottom: -1px;
|
||||||
box-shadow: 0 0 0 1px var(--swatch-color);
|
box-shadow: 0 0 0 1px var(--color-gray-30);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
filter: var(--theme-filter);
|
filter: var(--theme-filter);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +79,7 @@
|
||||||
bottom: var(--offset);
|
bottom: var(--offset);
|
||||||
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
||||||
z-index: 1; // due hover state so this has preference
|
z-index: 1; // due hover state so this has preference
|
||||||
border-radius: calc(var(--radius) + 1px);
|
border-radius: var(--radius);
|
||||||
filter: var(--theme-filter);
|
filter: var(--theme-filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,10 +134,11 @@
|
||||||
|
|
||||||
.color-picker__button__hotkey-label {
|
.color-picker__button__hotkey-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 5px;
|
||||||
bottom: 4px;
|
bottom: 3px;
|
||||||
filter: none;
|
filter: none;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker {
|
.color-picker {
|
||||||
|
|
|
@ -2,7 +2,11 @@ import * as Popover from "@radix-ui/react-popover";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
import { COLOR_PALETTE, isTransparent } from "@excalidraw/common";
|
import {
|
||||||
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
|
COLOR_PALETTE,
|
||||||
|
isTransparent,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common";
|
import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common";
|
||||||
|
|
||||||
|
@ -19,7 +23,7 @@ import { ColorInput } from "./ColorInput";
|
||||||
import { Picker } from "./Picker";
|
import { Picker } from "./Picker";
|
||||||
import PickerHeading from "./PickerHeading";
|
import PickerHeading from "./PickerHeading";
|
||||||
import { TopPicks } from "./TopPicks";
|
import { TopPicks } from "./TopPicks";
|
||||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
import { activeColorPickerSectionAtom, isColorDark } from "./colorPickerUtils";
|
||||||
|
|
||||||
import "./ColorPicker.scss";
|
import "./ColorPicker.scss";
|
||||||
|
|
||||||
|
@ -190,6 +194,7 @@ const ColorPickerTrigger = ({
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx("color-picker__button active-color properties-trigger", {
|
className={clsx("color-picker__button active-color properties-trigger", {
|
||||||
"is-transparent": color === "transparent" || !color,
|
"is-transparent": color === "transparent" || !color,
|
||||||
|
"has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
|
||||||
})}
|
})}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
style={color ? { "--swatch-color": color } : undefined}
|
style={color ? { "--swatch-color": color } : undefined}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const CustomColorList = ({
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"color-picker__button color-picker__button--large",
|
"color-picker__button color-picker__button--large has-outline",
|
||||||
{
|
{
|
||||||
active: color === c,
|
active: color === c,
|
||||||
"is-transparent": c === "transparent" || !c,
|
"is-transparent": c === "transparent" || !c,
|
||||||
|
@ -56,7 +56,7 @@ export const CustomColorList = ({
|
||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
|
<HotkeyLabel color={c} keyLabel={i + 1} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getContrastYIQ } from "./colorPickerUtils";
|
import { isColorDark } from "./colorPickerUtils";
|
||||||
|
|
||||||
interface HotkeyLabelProps {
|
interface HotkeyLabelProps {
|
||||||
color: string;
|
color: string;
|
||||||
keyLabel: string | number;
|
keyLabel: string | number;
|
||||||
isCustomColor?: boolean;
|
|
||||||
isShade?: boolean;
|
isShade?: boolean;
|
||||||
}
|
}
|
||||||
const HotkeyLabel = ({
|
const HotkeyLabel = ({
|
||||||
color,
|
color,
|
||||||
keyLabel,
|
keyLabel,
|
||||||
isCustomColor = false,
|
|
||||||
isShade = false,
|
isShade = false,
|
||||||
}: HotkeyLabelProps) => {
|
}: HotkeyLabelProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="color-picker__button__hotkey-label"
|
className="color-picker__button__hotkey-label"
|
||||||
style={{
|
style={{
|
||||||
color: getContrastYIQ(color, isCustomColor),
|
color: isColorDark(color) ? "#fff" : "#000",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isShade && "⇧"}
|
{isShade && "⇧"}
|
||||||
|
|
|
@ -65,7 +65,7 @@ const PickerColorList = ({
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"color-picker__button color-picker__button--large",
|
"color-picker__button color-picker__button--large has-outline",
|
||||||
{
|
{
|
||||||
active: colorObj?.colorName === key,
|
active: colorObj?.colorName === key,
|
||||||
"is-transparent": color === "transparent" || !color,
|
"is-transparent": color === "transparent" || !color,
|
||||||
|
|
|
@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
||||||
key={i}
|
key={i}
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"color-picker__button color-picker__button--large",
|
"color-picker__button color-picker__button--large has-outline",
|
||||||
{ active: i === shade },
|
{ active: i === shade },
|
||||||
)}
|
)}
|
||||||
aria-label="Shade"
|
aria-label="Shade"
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
DEFAULT_CANVAS_BACKGROUND_PICKS,
|
DEFAULT_CANVAS_BACKGROUND_PICKS,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
DEFAULT_ELEMENT_STROKE_PICKS,
|
DEFAULT_ELEMENT_STROKE_PICKS,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { isColorDark } from "./colorPickerUtils";
|
||||||
|
|
||||||
import type { ColorPickerType } from "./colorPickerUtils";
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
|
|
||||||
interface TopPicksProps {
|
interface TopPicksProps {
|
||||||
|
@ -51,6 +54,10 @@ export const TopPicks = ({
|
||||||
className={clsx("color-picker__button", {
|
className={clsx("color-picker__button", {
|
||||||
active: color === activeColor,
|
active: color === activeColor,
|
||||||
"is-transparent": color === "transparent" || !color,
|
"is-transparent": color === "transparent" || !color,
|
||||||
|
"has-outline": !isColorDark(
|
||||||
|
color,
|
||||||
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
|
),
|
||||||
})}
|
})}
|
||||||
style={{ "--swatch-color": color }}
|
style={{ "--swatch-color": color }}
|
||||||
key={color}
|
key={color}
|
||||||
|
|
|
@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType =
|
||||||
export const activeColorPickerSectionAtom =
|
export const activeColorPickerSectionAtom =
|
||||||
atom<ActiveColorPickerSectionAtomType>(null);
|
atom<ActiveColorPickerSectionAtomType>(null);
|
||||||
|
|
||||||
const calculateContrast = (r: number, g: number, b: number) => {
|
const calculateContrast = (r: number, g: number, b: number): number => {
|
||||||
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
return yiq >= 160 ? "black" : "white";
|
return yiq;
|
||||||
};
|
};
|
||||||
|
|
||||||
// inspiration from https://stackoverflow.com/a/11868398
|
// YIQ algo, inspiration from https://stackoverflow.com/a/11868398
|
||||||
export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
|
export const isColorDark = (color: string, threshold = 160): boolean => {
|
||||||
if (isCustomColor) {
|
// no color ("") -> assume it default to black
|
||||||
const style = new Option().style;
|
if (!color) {
|
||||||
style.color = bgHex;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (style.color) {
|
if (color === "transparent") {
|
||||||
const rgb = style.color
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a string color (white etc) or any other format -> convert to rgb by way
|
||||||
|
// of creating a DOM node and retrieving the computeStyle
|
||||||
|
if (!color.startsWith("#")) {
|
||||||
|
const node = document.createElement("div");
|
||||||
|
node.style.color = color;
|
||||||
|
|
||||||
|
if (node.style.color) {
|
||||||
|
// making invisible so document doesn't reflow (hopefully).
|
||||||
|
// display=none works too, but supposedly not in all browsers
|
||||||
|
node.style.position = "absolute";
|
||||||
|
node.style.visibility = "hidden";
|
||||||
|
node.style.width = "0";
|
||||||
|
node.style.height = "0";
|
||||||
|
|
||||||
|
// needs to be in DOM else browser won't compute the style
|
||||||
|
document.body.appendChild(node);
|
||||||
|
const computedColor = getComputedStyle(node).color;
|
||||||
|
document.body.removeChild(node);
|
||||||
|
// computed style is in rgb() format
|
||||||
|
const rgb = computedColor
|
||||||
.replace(/^(rgb|rgba)\(/, "")
|
.replace(/^(rgb|rgba)\(/, "")
|
||||||
.replace(/\)$/, "")
|
.replace(/\)$/, "")
|
||||||
.replace(/\s/g, "")
|
.replace(/\s/g, "")
|
||||||
|
@ -114,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
|
||||||
const g = parseInt(rgb[1]);
|
const g = parseInt(rgb[1]);
|
||||||
const b = parseInt(rgb[2]);
|
const b = parseInt(rgb[2]);
|
||||||
|
|
||||||
return calculateContrast(r, g, b);
|
return calculateContrast(r, g, b) < threshold;
|
||||||
}
|
}
|
||||||
|
// invalid color -> assume it default to black
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ? is this wanted?
|
const r = parseInt(color.slice(1, 3), 16);
|
||||||
if (bgHex === "transparent") {
|
const g = parseInt(color.slice(3, 5), 16);
|
||||||
return "black";
|
const b = parseInt(color.slice(5, 7), 16);
|
||||||
}
|
|
||||||
|
|
||||||
const r = parseInt(bgHex.substring(1, 3), 16);
|
return calculateContrast(r, g, b) < threshold;
|
||||||
const g = parseInt(bgHex.substring(3, 5), 16);
|
|
||||||
const b = parseInt(bgHex.substring(5, 7), 16);
|
|
||||||
|
|
||||||
return calculateContrast(r, g, b);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ColorPickerType =
|
export type ColorPickerType =
|
||||||
|
|
|
@ -317,6 +317,7 @@ function CommandPaletteInner({
|
||||||
const toolCommands: CommandPaletteItem[] = [
|
const toolCommands: CommandPaletteItem[] = [
|
||||||
actionManager.actions.toggleHandTool,
|
actionManager.actions.toggleHandTool,
|
||||||
actionManager.actions.setFrameAsActiveTool,
|
actionManager.actions.setFrameAsActiveTool,
|
||||||
|
actionManager.actions.toggleLassoTool,
|
||||||
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
|
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
|
||||||
|
|
||||||
const editorCommands: CommandPaletteItem[] = [
|
const editorCommands: CommandPaletteItem[] = [
|
||||||
|
|
|
@ -120,7 +120,7 @@ const getHints = ({
|
||||||
!appState.editingTextElement &&
|
!appState.editingTextElement &&
|
||||||
!appState.editingLinearElement
|
!appState.editingLinearElement
|
||||||
) {
|
) {
|
||||||
return t("hints.deepBoxSelect");
|
return [t("hints.deepBoxSelect")];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
|
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
|
||||||
|
@ -128,7 +128,7 @@ const getHints = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedElements.length && !isMobile) {
|
if (!selectedElements.length && !isMobile) {
|
||||||
return t("hints.canvasPanning");
|
return [t("hints.canvasPanning")];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: item.elements,
|
elements: item.elements,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
}).newElements,
|
}).duplicatedElements,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderScrollbars: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderInteractiveSceneCallback: (
|
renderInteractiveSceneCallback: (
|
||||||
data: RenderInteractiveSceneCallback,
|
data: RenderInteractiveSceneCallback,
|
||||||
|
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
remotePointerUsernames,
|
remotePointerUsernames,
|
||||||
remotePointerUserStates,
|
remotePointerUserStates,
|
||||||
selectionColor,
|
selectionColor,
|
||||||
renderScrollbars: false,
|
renderScrollbars: props.renderScrollbars,
|
||||||
},
|
},
|
||||||
device: props.device,
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
@ -230,7 +231,8 @@ const areEqual = (
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
prevProps.selectedElements !== nextProps.selectedElements
|
prevProps.selectedElements !== nextProps.selectedElements ||
|
||||||
|
prevProps.renderScrollbars !== nextProps.renderScrollbars
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,9 +87,8 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
||||||
return <div className="excalidraw__canvas-wrapper" ref={wrapperRef} />;
|
return <div className="excalidraw__canvas-wrapper" ref={wrapperRef} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRelevantAppStateProps = (
|
const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
|
||||||
appState: AppState,
|
const relevantAppStateProps = {
|
||||||
): StaticCanvasAppState => ({
|
|
||||||
zoom: appState.zoom,
|
zoom: appState.zoom,
|
||||||
scrollX: appState.scrollX,
|
scrollX: appState.scrollX,
|
||||||
scrollY: appState.scrollY,
|
scrollY: appState.scrollY,
|
||||||
|
@ -114,7 +113,10 @@ const getRelevantAppStateProps = (
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return relevantAppStateProps;
|
||||||
|
};
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
prevProps: StaticCanvasProps,
|
prevProps: StaticCanvasProps,
|
||||||
|
|
|
@ -274,6 +274,21 @@ export const SelectionIcon = createIcon(
|
||||||
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
|
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const LassoIcon = createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path d="M4.028 13.252c-.657 -.972 -1.028 -2.078 -1.028 -3.252c0 -3.866 4.03 -7 9 -7s9 3.134 9 7s-4.03 7 -9 7c-1.913 0 -3.686 -.464 -5.144 -1.255" />
|
||||||
|
<path d="M5 15m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M5 17c0 1.42 .316 2.805 1 4" />
|
||||||
|
</g>,
|
||||||
|
|
||||||
|
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
|
||||||
|
);
|
||||||
|
|
||||||
// tabler-icons: square
|
// tabler-icons: square
|
||||||
export const RectangleIcon = createIcon(
|
export const RectangleIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
|
@ -406,7 +421,7 @@ export const TrashIcon = createIcon(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EmbedIcon = createIcon(
|
export const EmbedIcon = createIcon(
|
||||||
<g strokeWidth="1.25">
|
<g strokeWidth="1.5">
|
||||||
<polyline points="12 16 18 10 12 4" />
|
<polyline points="12 16 18 10 12 4" />
|
||||||
<polyline points="8 4 2 10 8 16" />
|
<polyline points="8 4 2 10 8 16" />
|
||||||
</g>,
|
</g>,
|
||||||
|
|
|
@ -173,7 +173,7 @@ body.excalidraw-cursor-resize * {
|
||||||
.buttonList {
|
.buttonList {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 0.375rem;
|
column-gap: 0.5rem;
|
||||||
row-gap: 0.5rem;
|
row-gap: 0.5rem;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
@ -386,16 +386,10 @@ body.excalidraw-cursor-resize * {
|
||||||
|
|
||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem 0.75rem 0.25rem 0.75rem;
|
padding: 0.75rem;
|
||||||
width: 11.875rem;
|
width: 12.5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
.buttonList label,
|
|
||||||
.buttonList button,
|
|
||||||
.buttonList .zIndexButton {
|
|
||||||
--button-bg: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
--color-surface-high: #f1f0ff;
|
--color-surface-high: #f1f0ff;
|
||||||
--color-surface-mid: #f2f2f7;
|
--color-surface-mid: #f6f6f9;
|
||||||
--color-surface-low: #ececf4;
|
--color-surface-low: #ececf4;
|
||||||
--color-surface-lowest: #ffffff;
|
--color-surface-lowest: #ffffff;
|
||||||
--color-on-surface: #1b1b1f;
|
--color-on-surface: #1b1b1f;
|
||||||
|
@ -252,7 +252,7 @@
|
||||||
|
|
||||||
--color-logo-text: #e2dfff;
|
--color-logo-text: #e2dfff;
|
||||||
|
|
||||||
--color-surface-high: hsl(245, 10%, 21%);
|
--color-surface-high: #2e2d39;
|
||||||
--color-surface-low: hsl(240, 8%, 15%);
|
--color-surface-low: hsl(240, 8%, 15%);
|
||||||
--color-surface-mid: hsl(240 6% 10%);
|
--color-surface-mid: hsl(240 6% 10%);
|
||||||
--color-surface-lowest: hsl(0, 0%, 7%);
|
--color-surface-lowest: hsl(0, 0%, 7%);
|
||||||
|
|
|
@ -104,12 +104,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0.5,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
394.5,
|
394,
|
||||||
34.5,
|
34,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -129,8 +129,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 395,
|
"width": 395,
|
||||||
"x": 247,
|
"x": 247.5,
|
||||||
"y": 420,
|
"y": 420.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -160,11 +160,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
399.5,
|
399,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -185,7 +185,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"x": 227,
|
"x": 227.5,
|
||||||
"y": 450,
|
"y": 450,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -350,11 +350,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -375,7 +375,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -452,11 +452,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -477,7 +477,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -628,11 +628,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -653,7 +653,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -845,11 +845,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100.5,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -893,11 +893,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -914,7 +914,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 450,
|
"x": 450.5,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1490,11 +1490,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
272.485,
|
271.985,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -1517,7 +1517,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 272.985,
|
"width": 272.985,
|
||||||
"x": 111.262,
|
"x": 111.762,
|
||||||
"y": 57,
|
"y": 57,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1862,11 +1862,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -1883,7 +1883,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100.5,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1915,11 +1915,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -1936,7 +1936,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100.5,
|
||||||
"y": 200,
|
"y": 200,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1968,11 +1968,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -1989,7 +1989,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100.5,
|
||||||
"y": 300,
|
"y": 300,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -2021,11 +2021,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0.5,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99.5,
|
99,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -2042,7 +2042,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100.5,
|
||||||
"y": 400,
|
"y": 400,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
isFirefox,
|
isFirefox,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
cloneJSON,
|
cloneJSON,
|
||||||
|
SVG_DOCUMENT_PREAMBLE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
@ -134,7 +135,11 @@ export const exportCanvas = async (
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
return fileSave(
|
return fileSave(
|
||||||
svgPromise.then((svg) => {
|
svgPromise.then((svg) => {
|
||||||
return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
|
// adding SVG preamble so that older software parse the SVG file
|
||||||
|
// properly
|
||||||
|
return new Blob([SVG_DOCUMENT_PREAMBLE + svg.outerHTML], {
|
||||||
|
type: MIME_TYPES.svg,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
description: "Export to SVG",
|
description: "Export to SVG",
|
||||||
|
|
|
@ -86,6 +86,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||||
boolean
|
boolean
|
||||||
> = {
|
> = {
|
||||||
selection: true,
|
selection: true,
|
||||||
|
lasso: true,
|
||||||
text: true,
|
text: true,
|
||||||
rectangle: true,
|
rectangle: true,
|
||||||
diamond: true,
|
diamond: true,
|
||||||
|
@ -438,7 +439,7 @@ const repairContainerElement = (
|
||||||
// if defined, lest boundElements is stale
|
// if defined, lest boundElements is stale
|
||||||
!boundElement.containerId
|
!boundElement.containerId
|
||||||
) {
|
) {
|
||||||
(boundElement as Mutable<ExcalidrawTextElement>).containerId =
|
(boundElement as Mutable<typeof boundElement>).containerId =
|
||||||
container.id;
|
container.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,6 +464,10 @@ const repairBoundElement = (
|
||||||
? elementsMap.get(boundElement.containerId)
|
? elementsMap.get(boundElement.containerId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
(boundElement as Mutable<typeof boundElement>).angle = (
|
||||||
|
isArrowElement(container) ? 0 : container?.angle ?? 0
|
||||||
|
) as Radians;
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
boundElement.containerId = null;
|
boundElement.containerId = null;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -427,7 +427,7 @@ describe("Test Transform", () => {
|
||||||
const [arrow, text, rectangle, ellipse] = excalidrawElements;
|
const [arrow, text, rectangle, ellipse] = excalidrawElements;
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255.5,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [{ id: text.id, type: "text" }],
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
|
@ -512,7 +512,7 @@ describe("Test Transform", () => {
|
||||||
|
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255.5,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [{ id: text1.id, type: "text" }],
|
boundElements: [{ id: text1.id, type: "text" }],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
|
@ -730,7 +730,7 @@ describe("Test Transform", () => {
|
||||||
const [, , arrow, text] = excalidrawElements;
|
const [, , arrow, text] = excalidrawElements;
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255.5,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -36,6 +36,8 @@ import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -463,7 +465,13 @@ const bindLinearElementToElement = (
|
||||||
newPoints[endPointIndex][1] += delta;
|
newPoints[endPointIndex][1] += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(linearElement, { points: newPoints });
|
Object.assign(
|
||||||
|
linearElement,
|
||||||
|
LinearElementEditor.getNormalizedPoints({
|
||||||
|
...linearElement,
|
||||||
|
points: newPoints,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linearElement,
|
linearElement,
|
||||||
|
|
243
packages/excalidraw/eraser/index.ts
Normal file
243
packages/excalidraw/eraser/index.ts
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||||
|
import { getElementLineSegments } from "@excalidraw/element/bounds";
|
||||||
|
import {
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentIntersectionPoints,
|
||||||
|
pointFrom,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import { getElementShape } from "@excalidraw/element/shapes";
|
||||||
|
import { shouldTestInside } from "@excalidraw/element/collision";
|
||||||
|
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||||
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
import { getBoundTextElementId } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
|
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||||
|
import type {
|
||||||
|
ElementsSegmentsMap,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
} from "@excalidraw/math/types";
|
||||||
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
|
|
||||||
|
import type { AnimationFrameHandler } from "../animation-frame-handler";
|
||||||
|
|
||||||
|
import type App from "../components/App";
|
||||||
|
|
||||||
|
// just enough to form a segment; this is sufficient for eraser
|
||||||
|
const POINTS_ON_TRAIL = 2;
|
||||||
|
|
||||||
|
export class EraserTrail extends AnimatedTrail {
|
||||||
|
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||||
|
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
|
super(animationFrameHandler, app, {
|
||||||
|
streamline: 0.2,
|
||||||
|
size: 5,
|
||||||
|
keepHead: true,
|
||||||
|
sizeMapping: (c) => {
|
||||||
|
const DECAY_TIME = 200;
|
||||||
|
const DECAY_LENGTH = 10;
|
||||||
|
const t = Math.max(
|
||||||
|
0,
|
||||||
|
1 - (performance.now() - c.pressure) / DECAY_TIME,
|
||||||
|
);
|
||||||
|
const l =
|
||||||
|
(DECAY_LENGTH -
|
||||||
|
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
|
||||||
|
DECAY_LENGTH;
|
||||||
|
|
||||||
|
return Math.min(easeOut(l), easeOut(t));
|
||||||
|
},
|
||||||
|
fill: () =>
|
||||||
|
app.state.theme === THEME.LIGHT
|
||||||
|
? "rgba(0, 0, 0, 0.2)"
|
||||||
|
: "rgba(255, 255, 255, 0.2)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startPath(x: number, y: number): void {
|
||||||
|
this.endPath();
|
||||||
|
super.startPath(x, y);
|
||||||
|
this.elementsToErase.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
addPointToPath(x: number, y: number, restore = false) {
|
||||||
|
super.addPointToPath(x, y);
|
||||||
|
|
||||||
|
const elementsToEraser = this.updateElementsToBeErased(restore);
|
||||||
|
|
||||||
|
return elementsToEraser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateElementsToBeErased(restoreToErase?: boolean) {
|
||||||
|
let eraserPath: GlobalPoint[] =
|
||||||
|
super
|
||||||
|
.getCurrentTrail()
|
||||||
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
||||||
|
|
||||||
|
// for efficiency and avoid unnecessary calculations,
|
||||||
|
// take only POINTS_ON_TRAIL points to form some number of segments
|
||||||
|
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
|
||||||
|
|
||||||
|
const candidateElements = this.app.visibleElements.filter(
|
||||||
|
(el) => !el.locked,
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidateElementsMap = arrayToMap(candidateElements);
|
||||||
|
|
||||||
|
const pathSegments = eraserPath.reduce((acc, point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.push(lineSegment(eraserPath[index - 1], point));
|
||||||
|
return acc;
|
||||||
|
}, [] as LineSegment<GlobalPoint>[]);
|
||||||
|
|
||||||
|
if (pathSegments.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of candidateElements) {
|
||||||
|
// restore only if already added to the to-be-erased set
|
||||||
|
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
||||||
|
const intersects = eraserTest(
|
||||||
|
pathSegments,
|
||||||
|
element,
|
||||||
|
this.segmentsCache,
|
||||||
|
this.geometricShapesCache,
|
||||||
|
candidateElementsMap,
|
||||||
|
this.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||||
|
|
||||||
|
if (this.groupsToErase.has(shallowestGroupId)) {
|
||||||
|
const elementsInGroup = getElementsInGroup(
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
shallowestGroupId,
|
||||||
|
);
|
||||||
|
for (const elementInGroup of elementsInGroup) {
|
||||||
|
this.elementsToErase.delete(elementInGroup.id);
|
||||||
|
}
|
||||||
|
this.groupsToErase.delete(shallowestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoundToContainer(element)) {
|
||||||
|
this.elementsToErase.delete(element.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBoundTextElement(element)) {
|
||||||
|
const boundText = getBoundTextElementId(element);
|
||||||
|
|
||||||
|
if (boundText) {
|
||||||
|
this.elementsToErase.delete(boundText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elementsToErase.delete(element.id);
|
||||||
|
}
|
||||||
|
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
||||||
|
const intersects = eraserTest(
|
||||||
|
pathSegments,
|
||||||
|
element,
|
||||||
|
this.segmentsCache,
|
||||||
|
this.geometricShapesCache,
|
||||||
|
candidateElementsMap,
|
||||||
|
this.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||||
|
|
||||||
|
if (!this.groupsToErase.has(shallowestGroupId)) {
|
||||||
|
const elementsInGroup = getElementsInGroup(
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
shallowestGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const elementInGroup of elementsInGroup) {
|
||||||
|
this.elementsToErase.add(elementInGroup.id);
|
||||||
|
}
|
||||||
|
this.groupsToErase.add(shallowestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBoundTextElement(element)) {
|
||||||
|
const boundText = getBoundTextElementId(element);
|
||||||
|
|
||||||
|
if (boundText) {
|
||||||
|
this.elementsToErase.add(boundText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoundToContainer(element)) {
|
||||||
|
this.elementsToErase.add(element.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elementsToErase.add(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.elementsToErase);
|
||||||
|
}
|
||||||
|
|
||||||
|
endPath(): void {
|
||||||
|
super.endPath();
|
||||||
|
super.clearTrails();
|
||||||
|
this.elementsToErase.clear();
|
||||||
|
this.groupsToErase.clear();
|
||||||
|
this.segmentsCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eraserTest = (
|
||||||
|
pathSegments: LineSegment<GlobalPoint>[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
app: App,
|
||||||
|
): boolean => {
|
||||||
|
let shape = shapesCache.get(element.id);
|
||||||
|
|
||||||
|
if (!shape) {
|
||||||
|
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
||||||
|
shapesCache.set(element.id, shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||||
|
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elementSegments = elementsSegments.get(element.id);
|
||||||
|
|
||||||
|
if (!elementSegments) {
|
||||||
|
elementSegments = getElementLineSegments(element, elementsMap);
|
||||||
|
elementsSegments.set(element.id, elementSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathSegments.some((pathSegment) =>
|
||||||
|
elementSegments?.some(
|
||||||
|
(elementSegment) =>
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
pathSegment,
|
||||||
|
elementSegment,
|
||||||
|
app.getElementHitThreshold(),
|
||||||
|
) !== null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
|
renderScrollbars,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
|
renderScrollbars={renderScrollbars}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|
201
packages/excalidraw/lasso/index.ts
Normal file
201
packages/excalidraw/lasso/index.ts
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import {
|
||||||
|
type GlobalPoint,
|
||||||
|
type LineSegment,
|
||||||
|
pointFrom,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { getElementLineSegments } from "@excalidraw/element/bounds";
|
||||||
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
import {
|
||||||
|
isFrameLikeElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import { getFrameChildren } from "@excalidraw/element/frame";
|
||||||
|
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
|
import { arrayToMap, easeOut } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { type AnimationFrameHandler } from "../animation-frame-handler";
|
||||||
|
|
||||||
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
|
|
||||||
|
import { getLassoSelectedElementIds } from "./utils";
|
||||||
|
|
||||||
|
import type App from "../components/App";
|
||||||
|
|
||||||
|
export class LassoTrail extends AnimatedTrail {
|
||||||
|
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null =
|
||||||
|
null;
|
||||||
|
private keepPreviousSelection: boolean = false;
|
||||||
|
|
||||||
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
|
super(animationFrameHandler, app, {
|
||||||
|
animateTrail: true,
|
||||||
|
streamline: 0.4,
|
||||||
|
sizeMapping: (c) => {
|
||||||
|
const DECAY_TIME = Infinity;
|
||||||
|
const DECAY_LENGTH = 5000;
|
||||||
|
const t = Math.max(
|
||||||
|
0,
|
||||||
|
1 - (performance.now() - c.pressure) / DECAY_TIME,
|
||||||
|
);
|
||||||
|
const l =
|
||||||
|
(DECAY_LENGTH -
|
||||||
|
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
|
||||||
|
DECAY_LENGTH;
|
||||||
|
|
||||||
|
return Math.min(easeOut(l), easeOut(t));
|
||||||
|
},
|
||||||
|
fill: () => "rgba(105,101,219,0.05)",
|
||||||
|
stroke: () => "rgba(105,101,219)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startPath(x: number, y: number, keepPreviousSelection = false) {
|
||||||
|
// clear any existing trails just in case
|
||||||
|
this.endPath();
|
||||||
|
|
||||||
|
super.startPath(x, y);
|
||||||
|
this.intersectedElements.clear();
|
||||||
|
this.enclosedElements.clear();
|
||||||
|
|
||||||
|
this.keepPreviousSelection = keepPreviousSelection;
|
||||||
|
|
||||||
|
if (!this.keepPreviousSelection) {
|
||||||
|
this.app.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
selectedLinearElement: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectElementsFromIds = (ids: string[]) => {
|
||||||
|
this.app.setState((prevState) => {
|
||||||
|
const nextSelectedElementIds = ids.reduce((acc, id) => {
|
||||||
|
acc[id] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<ExcalidrawElement["id"], true>);
|
||||||
|
|
||||||
|
if (this.keepPreviousSelection) {
|
||||||
|
for (const id of Object.keys(prevState.selectedElementIds)) {
|
||||||
|
nextSelectedElementIds[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id] of Object.entries(nextSelectedElementIds)) {
|
||||||
|
const element = this.app.scene.getNonDeletedElement(id);
|
||||||
|
|
||||||
|
if (element && isTextElement(element)) {
|
||||||
|
const container = getContainerElement(
|
||||||
|
element,
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
if (container) {
|
||||||
|
nextSelectedElementIds[container.id] = true;
|
||||||
|
delete nextSelectedElementIds[element.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all children of selected frames
|
||||||
|
for (const [id] of Object.entries(nextSelectedElementIds)) {
|
||||||
|
const element = this.app.scene.getNonDeletedElement(id);
|
||||||
|
|
||||||
|
if (element && isFrameLikeElement(element)) {
|
||||||
|
const elementsInFrame = getFrameChildren(
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
element.id,
|
||||||
|
);
|
||||||
|
for (const child of elementsInFrame) {
|
||||||
|
delete nextSelectedElementIds[child.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSelection = selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: prevState.editingGroupId,
|
||||||
|
selectedElementIds: nextSelectedElementIds,
|
||||||
|
},
|
||||||
|
this.app.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedIds = [...Object.keys(nextSelection.selectedElementIds)];
|
||||||
|
const selectedGroupIds = [...Object.keys(nextSelection.selectedGroupIds)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedElementIds: nextSelection.selectedElementIds,
|
||||||
|
selectedGroupIds: nextSelection.selectedGroupIds,
|
||||||
|
selectedLinearElement:
|
||||||
|
selectedIds.length === 1 &&
|
||||||
|
!selectedGroupIds.length &&
|
||||||
|
isLinearElement(this.app.scene.getNonDeletedElement(selectedIds[0]))
|
||||||
|
? new LinearElementEditor(
|
||||||
|
this.app.scene.getNonDeletedElement(
|
||||||
|
selectedIds[0],
|
||||||
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addPointToPath = (x: number, y: number, keepPreviousSelection = false) => {
|
||||||
|
super.addPointToPath(x, y);
|
||||||
|
|
||||||
|
this.keepPreviousSelection = keepPreviousSelection;
|
||||||
|
|
||||||
|
this.updateSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateSelection = () => {
|
||||||
|
const lassoPath = super
|
||||||
|
.getCurrentTrail()
|
||||||
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
|
||||||
|
|
||||||
|
if (!this.elementsSegments) {
|
||||||
|
this.elementsSegments = new Map();
|
||||||
|
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
||||||
|
for (const element of this.app.visibleElements) {
|
||||||
|
const segments = getElementLineSegments(element, visibleElementsMap);
|
||||||
|
this.elementsSegments.set(element.id, segments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lassoPath) {
|
||||||
|
const { selectedElementIds } = getLassoSelectedElementIds({
|
||||||
|
lassoPath,
|
||||||
|
elements: this.app.visibleElements,
|
||||||
|
elementsSegments: this.elementsSegments,
|
||||||
|
intersectedElements: this.intersectedElements,
|
||||||
|
enclosedElements: this.enclosedElements,
|
||||||
|
simplifyDistance: 5 / this.app.state.zoom.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectElementsFromIds(selectedElementIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
endPath(): void {
|
||||||
|
super.endPath();
|
||||||
|
super.clearTrails();
|
||||||
|
this.intersectedElements.clear();
|
||||||
|
this.enclosedElements.clear();
|
||||||
|
this.elementsSegments = null;
|
||||||
|
}
|
||||||
|
}
|
109
packages/excalidraw/lasso/utils.ts
Normal file
109
packages/excalidraw/lasso/utils.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
|
import {
|
||||||
|
polygonFromPoints,
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentIntersectionPoints,
|
||||||
|
polygonIncludesPointNonZero,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ElementsSegmentsMap,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
} from "@excalidraw/math/types";
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
export const getLassoSelectedElementIds = (input: {
|
||||||
|
lassoPath: GlobalPoint[];
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
elementsSegments: ElementsSegmentsMap;
|
||||||
|
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||||
|
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||||
|
simplifyDistance?: number;
|
||||||
|
}): {
|
||||||
|
selectedElementIds: string[];
|
||||||
|
} => {
|
||||||
|
const {
|
||||||
|
lassoPath,
|
||||||
|
elements,
|
||||||
|
elementsSegments,
|
||||||
|
intersectedElements,
|
||||||
|
enclosedElements,
|
||||||
|
simplifyDistance,
|
||||||
|
} = input;
|
||||||
|
// simplify the path to reduce the number of points
|
||||||
|
let path: GlobalPoint[] = lassoPath;
|
||||||
|
if (simplifyDistance) {
|
||||||
|
path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
|
||||||
|
}
|
||||||
|
// as the path might not enclose a shape anymore, clear before checking
|
||||||
|
enclosedElements.clear();
|
||||||
|
for (const element of elements) {
|
||||||
|
if (
|
||||||
|
!intersectedElements.has(element.id) &&
|
||||||
|
!enclosedElements.has(element.id)
|
||||||
|
) {
|
||||||
|
const enclosed = enclosureTest(path, element, elementsSegments);
|
||||||
|
if (enclosed) {
|
||||||
|
enclosedElements.add(element.id);
|
||||||
|
} else {
|
||||||
|
const intersects = intersectionTest(path, element, elementsSegments);
|
||||||
|
if (intersects) {
|
||||||
|
intersectedElements.add(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [...intersectedElements, ...enclosedElements];
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedElementIds: results,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const enclosureTest = (
|
||||||
|
lassoPath: GlobalPoint[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
): boolean => {
|
||||||
|
const lassoPolygon = polygonFromPoints(lassoPath);
|
||||||
|
const segments = elementsSegments.get(element.id);
|
||||||
|
if (!segments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.some((segment) => {
|
||||||
|
return segment.some((point) =>
|
||||||
|
polygonIncludesPointNonZero(point, lassoPolygon),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const intersectionTest = (
|
||||||
|
lassoPath: GlobalPoint[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
): boolean => {
|
||||||
|
const elementSegments = elementsSegments.get(element.id);
|
||||||
|
if (!elementSegments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||||
|
return acc;
|
||||||
|
}, [] as LineSegment<GlobalPoint>[]);
|
||||||
|
|
||||||
|
return lassoSegments.some((lassoSegment) =>
|
||||||
|
elementSegments.some(
|
||||||
|
(elementSegment) =>
|
||||||
|
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||||
|
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -278,6 +278,7 @@
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
|
"lasso": "Lasso selection",
|
||||||
"image": "Insert image",
|
"image": "Insert image",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Diamond",
|
"diamond": "Diamond",
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
|
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
|
||||||
"@excalidraw/random-username": "1.1.0",
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@radix-ui/react-popover": "1.1.6",
|
"@radix-ui/react-popover": "1.1.6",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.1.3",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
|
|
|
@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
|
||||||
let scrollBars;
|
let scrollBars;
|
||||||
if (renderConfig.renderScrollbars) {
|
if (renderConfig.renderScrollbars) {
|
||||||
scrollBars = getScrollBars(
|
scrollBars = getScrollBars(
|
||||||
visibleElements,
|
elementsMap,
|
||||||
normalizedWidth,
|
normalizedWidth,
|
||||||
normalizedHeight,
|
normalizedHeight,
|
||||||
appState,
|
appState,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
isReadonlyArray,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||||
|
@ -292,9 +293,7 @@ class Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
const _nextElements =
|
const _nextElements = isReadonlyArray(nextElements)
|
||||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
|
||||||
nextElements instanceof Array
|
|
||||||
? nextElements
|
? nextElements
|
||||||
: Array.from(nextElements.values());
|
: Array.from(nextElements.values());
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
|
@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
|
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
import type { ScrollBars } from "./types";
|
import type { RenderableElementsMap, ScrollBars } from "./types";
|
||||||
|
|
||||||
export const SCROLLBAR_MARGIN = 4;
|
export const SCROLLBAR_MARGIN = 4;
|
||||||
export const SCROLLBAR_WIDTH = 6;
|
export const SCROLLBAR_WIDTH = 6;
|
||||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||||
|
|
||||||
|
// The scrollbar represents where the viewport is in relationship to the scene
|
||||||
export const getScrollBars = (
|
export const getScrollBars = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: RenderableElementsMap,
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (!elements.length) {
|
if (!elements.size) {
|
||||||
return {
|
return {
|
||||||
horizontal: null,
|
horizontal: null,
|
||||||
vertical: null,
|
vertical: null,
|
||||||
|
@ -33,9 +32,6 @@ export const getScrollBars = (
|
||||||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
|
||||||
|
|
||||||
const safeArea = {
|
const safeArea = {
|
||||||
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
||||||
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
||||||
|
@ -46,10 +42,8 @@ export const getScrollBars = (
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
// The viewport is the rectangle currently visible for the user
|
// The viewport is the rectangle currently visible for the user
|
||||||
const viewportMinX =
|
const viewportMinX = -appState.scrollX + safeArea.left;
|
||||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
const viewportMinY = -appState.scrollY + safeArea.top;
|
||||||
const viewportMinY =
|
|
||||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
|
||||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||||
|
|
||||||
|
@ -59,8 +53,43 @@ export const getScrollBars = (
|
||||||
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
||||||
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
||||||
|
|
||||||
// The scrollbar represents where the viewport is in relationship to the scene
|
// the elements-only bbox
|
||||||
|
const sceneWidth = elementsMaxX - elementsMinX;
|
||||||
|
const sceneHeight = elementsMaxY - elementsMinY;
|
||||||
|
|
||||||
|
// scene (elements) bbox + the viewport bbox that extends outside of it
|
||||||
|
const extendedSceneWidth = sceneMaxX - sceneMinX;
|
||||||
|
const extendedSceneHeight = sceneMaxY - sceneMinY;
|
||||||
|
|
||||||
|
const scrollWidthOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarWidth =
|
||||||
|
viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) -
|
||||||
|
scrollWidthOffset;
|
||||||
|
|
||||||
|
const scrollbarHeightOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarHeight =
|
||||||
|
viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) -
|
||||||
|
scrollbarHeightOffset;
|
||||||
|
// NOTE the delta multiplier calculation isn't quite correct when viewport
|
||||||
|
// is extended outside the scene (elements) bbox as there's some small
|
||||||
|
// accumulation error. I'll let this be an exercise for others to fix. ^^
|
||||||
|
const horizontalDeltaMultiplier =
|
||||||
|
extendedSceneWidth > sceneWidth
|
||||||
|
? (extendedSceneWidth * appState.zoom.value) /
|
||||||
|
(scrollbarWidth + scrollWidthOffset)
|
||||||
|
: viewportWidth / (scrollbarWidth + scrollWidthOffset);
|
||||||
|
|
||||||
|
const verticalDeltaMultiplier =
|
||||||
|
extendedSceneHeight > sceneHeight
|
||||||
|
? (extendedSceneHeight * appState.zoom.value) /
|
||||||
|
(scrollbarHeight + scrollbarHeightOffset)
|
||||||
|
: viewportHeight / (scrollbarHeight + scrollbarHeightOffset);
|
||||||
return {
|
return {
|
||||||
horizontal:
|
horizontal:
|
||||||
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
||||||
|
@ -68,18 +97,17 @@ export const getScrollBars = (
|
||||||
: {
|
: {
|
||||||
x:
|
x:
|
||||||
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
||||||
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
|
SCROLLBAR_WIDTH +
|
||||||
viewportWidth,
|
((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
|
||||||
y:
|
y:
|
||||||
viewportHeight -
|
viewportHeight -
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
||||||
width:
|
width: scrollbarWidth,
|
||||||
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
|
|
||||||
viewportWidth -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
|
|
||||||
height: SCROLLBAR_WIDTH,
|
height: SCROLLBAR_WIDTH,
|
||||||
|
deltaMultiplier: horizontalDeltaMultiplier,
|
||||||
},
|
},
|
||||||
|
|
||||||
vertical:
|
vertical:
|
||||||
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
||||||
? null
|
? null
|
||||||
|
@ -90,14 +118,13 @@ export const getScrollBars = (
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
||||||
y:
|
y:
|
||||||
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
|
Math.max(safeArea.top, SCROLLBAR_MARGIN) +
|
||||||
viewportHeight +
|
SCROLLBAR_WIDTH +
|
||||||
Math.max(safeArea.top, SCROLLBAR_MARGIN),
|
((viewportMinY - sceneMinY) / extendedSceneHeight) *
|
||||||
|
viewportHeight,
|
||||||
width: SCROLLBAR_WIDTH,
|
width: SCROLLBAR_WIDTH,
|
||||||
height:
|
height: scrollbarHeight,
|
||||||
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
|
deltaMultiplier: verticalDeltaMultiplier,
|
||||||
viewportHeight -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -130,12 +130,14 @@ export type ScrollBars = {
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
vertical: {
|
vertical: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1088,6 +1089,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1307,6 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1641,6 +1644,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1975,6 +1979,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2194,6 +2199,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2437,6 +2443,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2741,6 +2748,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3113,6 +3121,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3591,6 +3600,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3917,6 +3927,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4243,6 +4254,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4649,6 +4661,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5870,6 +5883,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7137,6 +7151,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7408,7 +7423,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
</svg>,
|
</svg>,
|
||||||
"label": "labels.elementLock.unlockAll",
|
"label": "labels.elementLock.unlockAll",
|
||||||
"name": "unlockAllElements",
|
"name": "unlockAllElements",
|
||||||
"paletteName": "Unlock all elements",
|
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
"predicate": [Function],
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
|
@ -7559,7 +7573,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"label": "buttons.zenMode",
|
"label": "buttons.zenMode",
|
||||||
"name": "zenMode",
|
"name": "zenMode",
|
||||||
"paletteName": "Toggle zen mode",
|
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
"predicate": [Function],
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
|
@ -7603,7 +7616,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"label": "labels.viewMode",
|
"label": "labels.viewMode",
|
||||||
"name": "viewMode",
|
"name": "viewMode",
|
||||||
"paletteName": "Toggle view mode",
|
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"predicate": [Function],
|
"predicate": [Function],
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
|
@ -7677,7 +7689,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
],
|
],
|
||||||
"label": "stats.fullTitle",
|
"label": "stats.fullTitle",
|
||||||
"name": "stats",
|
"name": "stats",
|
||||||
"paletteName": "Toggle stats",
|
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
"trackEvent": {
|
"trackEvent": {
|
||||||
"category": "menu",
|
"category": "menu",
|
||||||
|
@ -7814,6 +7825,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8802,6 +8814,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
|
|
@ -572,7 +572,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="color-picker__top-picks"
|
class="color-picker__top-picks"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button active"
|
class="color-picker__button active has-outline"
|
||||||
data-testid="color-top-pick-#ffffff"
|
data-testid="color-top-pick-#ffffff"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="#ffffff"
|
title="#ffffff"
|
||||||
|
@ -583,7 +583,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button has-outline"
|
||||||
data-testid="color-top-pick-#f8f9fa"
|
data-testid="color-top-pick-#f8f9fa"
|
||||||
style="--swatch-color: #f8f9fa;"
|
style="--swatch-color: #f8f9fa;"
|
||||||
title="#f8f9fa"
|
title="#f8f9fa"
|
||||||
|
@ -594,7 +594,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button has-outline"
|
||||||
data-testid="color-top-pick-#f5faff"
|
data-testid="color-top-pick-#f5faff"
|
||||||
style="--swatch-color: #f5faff;"
|
style="--swatch-color: #f5faff;"
|
||||||
title="#f5faff"
|
title="#f5faff"
|
||||||
|
@ -605,7 +605,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button has-outline"
|
||||||
data-testid="color-top-pick-#fffce8"
|
data-testid="color-top-pick-#fffce8"
|
||||||
style="--swatch-color: #fffce8;"
|
style="--swatch-color: #fffce8;"
|
||||||
title="#fffce8"
|
title="#fffce8"
|
||||||
|
@ -616,7 +616,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="color-picker__button"
|
class="color-picker__button has-outline"
|
||||||
data-testid="color-top-pick-#fdf8f6"
|
data-testid="color-top-pick-#fdf8f6"
|
||||||
style="--swatch-color: #fdf8f6;"
|
style="--swatch-color: #fdf8f6;"
|
||||||
title="#fdf8f6"
|
title="#fdf8f6"
|
||||||
|
@ -635,7 +635,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label="Canvas background"
|
aria-label="Canvas background"
|
||||||
class="color-picker__button active-color properties-trigger"
|
class="color-picker__button active-color properties-trigger has-outline"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="Show background color picker"
|
title="Show background color picker"
|
||||||
|
|
|
@ -5,6 +5,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -604,6 +605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1111,6 +1113,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1482,6 +1485,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1854,6 +1858,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2124,6 +2129,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2563,6 +2569,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2865,6 +2872,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3152,6 +3160,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3449,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3738,6 +3748,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3976,6 +3987,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4238,6 +4250,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4514,6 +4527,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4748,6 +4762,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4982,6 +4997,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5214,6 +5230,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5446,6 +5463,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5708,6 +5726,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -6042,6 +6061,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -6470,6 +6490,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -6851,6 +6872,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7173,6 +7195,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7325,8 +7348,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -7399,8 +7422,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -7474,6 +7497,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7706,6 +7730,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8064,6 +8089,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8422,6 +8448,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8829,6 +8856,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
|
@ -9119,6 +9147,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9387,6 +9416,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9654,6 +9684,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9888,6 +9919,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -10192,6 +10224,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -10535,6 +10568,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -10773,6 +10807,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11225,6 +11260,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11482,6 +11518,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11724,6 +11761,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11968,6 +12006,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
|
@ -12099,8 +12138,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12153,8 +12192,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 60,
|
"x": 40,
|
||||||
"y": 0,
|
"y": -20,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12207,8 +12246,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12262,8 +12301,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -12348,8 +12387,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -12372,6 +12411,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -12622,6 +12662,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -12866,6 +12907,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13110,6 +13152,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13360,6 +13403,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13695,6 +13739,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13870,6 +13915,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14161,6 +14207,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14431,6 +14478,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14709,6 +14757,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14873,6 +14922,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -15570,6 +15620,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -16189,6 +16240,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -16808,6 +16860,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -17518,6 +17571,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -18265,6 +18319,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -18742,6 +18797,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -19267,6 +19323,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -19726,6 +19783,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
|
|
@ -1,40 +1,6 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 50,
|
|
||||||
"id": "id2",
|
|
||||||
"index": "Zz",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 238820263,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 6,
|
|
||||||
"versionNonce": 1604849351,
|
|
||||||
"width": 30,
|
|
||||||
"x": 30,
|
|
||||||
"y": 20,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
|
@ -61,7 +27,41 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 1505387817,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 50,
|
||||||
|
"id": "id2",
|
||||||
|
"index": "a1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": {
|
||||||
|
"type": 3,
|
||||||
|
},
|
||||||
|
"seed": 1604849351,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 7,
|
||||||
|
"versionNonce": 915032327,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
|
|
|
@ -5,6 +5,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -420,6 +421,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -826,6 +828,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1371,6 +1374,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1575,6 +1579,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -1950,6 +1955,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2032,7 +2038,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"searchMatches": [],
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
@ -2122,8 +2128,16 @@ History {
|
||||||
HistoryEntry {
|
HistoryEntry {
|
||||||
"appStateChange": AppStateChange {
|
"appStateChange": AppStateChange {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {},
|
"deleted": {
|
||||||
"inserted": {},
|
"selectedElementIds": {
|
||||||
|
"id2": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"selectedElementIds": {
|
||||||
|
"id0": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elementsChange": ElementsChange {
|
"elementsChange": ElementsChange {
|
||||||
|
@ -2139,7 +2153,7 @@ History {
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zz",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -2153,26 +2167,15 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 20,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": Map {
|
"updated": Map {},
|
||||||
"id0" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 20,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 10,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -2188,6 +2191,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2368,6 +2372,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2688,6 +2693,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -2934,6 +2940,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3177,6 +3184,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3407,6 +3415,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3663,6 +3672,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -3974,6 +3984,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4396,6 +4407,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4679,6 +4691,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -4932,6 +4945,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5142,6 +5156,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5341,6 +5356,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -5723,6 +5739,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -6013,6 +6030,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
|
@ -6821,6 +6839,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7151,6 +7170,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7427,6 +7447,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7661,6 +7682,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -7898,6 +7920,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8078,6 +8101,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8258,6 +8282,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8438,6 +8463,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8661,6 +8687,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -8883,6 +8910,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
|
@ -9077,6 +9105,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9300,6 +9329,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9480,6 +9510,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9702,6 +9733,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -9882,6 +9914,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
|
@ -10076,6 +10109,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -10256,6 +10290,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -10340,13 +10375,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"searchMatches": [],
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id6": true,
|
||||||
"id1": true,
|
"id8": true,
|
||||||
"id2": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {
|
"selectedGroupIds": {
|
||||||
"id4": true,
|
"id7": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
|
@ -10610,8 +10645,26 @@ History {
|
||||||
HistoryEntry {
|
HistoryEntry {
|
||||||
"appStateChange": AppStateChange {
|
"appStateChange": AppStateChange {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {},
|
"deleted": {
|
||||||
"inserted": {},
|
"selectedElementIds": {
|
||||||
|
"id6": true,
|
||||||
|
"id8": true,
|
||||||
|
"id9": true,
|
||||||
|
},
|
||||||
|
"selectedGroupIds": {
|
||||||
|
"id7": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"selectedElementIds": {
|
||||||
|
"id0": true,
|
||||||
|
"id1": true,
|
||||||
|
"id2": true,
|
||||||
|
},
|
||||||
|
"selectedGroupIds": {
|
||||||
|
"id4": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elementsChange": ElementsChange {
|
"elementsChange": ElementsChange {
|
||||||
|
@ -10629,7 +10682,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zx",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10643,8 +10696,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 20,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -10662,7 +10715,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zy",
|
"index": "a4",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10676,8 +10729,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 30,
|
"x": 40,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -10695,7 +10748,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zz",
|
"index": "a5",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10709,46 +10762,15 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 50,
|
"x": 60,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": Map {
|
"updated": Map {},
|
||||||
"id0" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 20,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 10,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id1" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 40,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 30,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id2" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 60,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 50,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -10764,6 +10786,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11041,6 +11064,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11167,6 +11191,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11366,6 +11391,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -11677,6 +11703,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -12089,6 +12116,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -12702,6 +12730,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -12831,6 +12860,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13415,6 +13445,7 @@ exports[`regression tests > switches from group of selected elements to another
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -13753,6 +13784,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14018,6 +14050,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14144,6 +14177,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
@ -14523,6 +14557,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -14649,6 +14684,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
|
|
@ -307,6 +307,41 @@ describe("pasting & frames", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove element from frame when pasted outside", async () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
frameId: frame.id,
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame]);
|
||||||
|
|
||||||
|
const clipboardJSON = await serializeAsClipboardJSON({
|
||||||
|
elements: [rect],
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
mouse.moveTo(150, 150);
|
||||||
|
|
||||||
|
pasteWithCtrlCmdV(clipboardJSON);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
expect(h.elements[1].type).toBe(rect.type);
|
||||||
|
expect(h.elements[1].frameId).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should filter out elements not overlapping frame", async () => {
|
it("should filter out elements not overlapping frame", async () => {
|
||||||
const frame = API.createElement({
|
const frame = API.createElement({
|
||||||
type: "frame",
|
type: "frame",
|
||||||
|
|
|
@ -218,7 +218,7 @@ describe("Cropping and other features", async () => {
|
||||||
initialHeight / 2,
|
initialHeight / 2,
|
||||||
]);
|
]);
|
||||||
Keyboard.keyDown(KEYS.ESCAPE);
|
Keyboard.keyDown(KEYS.ESCAPE);
|
||||||
const duplicatedImage = duplicateElement(null, new Map(), image, {});
|
const duplicatedImage = duplicateElement(null, new Map(), image);
|
||||||
act(() => {
|
act(() => {
|
||||||
h.app.scene.insertElement(duplicatedImage);
|
h.app.scene.insertElement(duplicatedImage);
|
||||||
});
|
});
|
||||||
|
|
|
@ -444,7 +444,6 @@ export class API {
|
||||||
|
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: "text2",
|
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 20,
|
height: 20,
|
||||||
containerId: arrow.id,
|
containerId: arrow.id,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
import { KEYS, arrayToMap } from "@excalidraw/common";
|
import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ export class Keyboard {
|
||||||
const getElementPointForSelection = (
|
const getElementPointForSelection = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, angle } = element;
|
||||||
const target = pointFrom<GlobalPoint>(
|
const target = pointFrom<GlobalPoint>(
|
||||||
x +
|
x +
|
||||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||||
|
@ -166,7 +166,7 @@ const getElementPointForSelection = (
|
||||||
(bounds[1] + bounds[3]) / 2,
|
(bounds[1] + bounds[3]) / 2,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
center = pointFrom(x + width / 2, y + height / 2);
|
center = elementCenterPoint(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
|
@ -180,10 +180,17 @@ export class Pointer {
|
||||||
public clientX = 0;
|
public clientX = 0;
|
||||||
public clientY = 0;
|
public clientY = 0;
|
||||||
|
|
||||||
|
static activePointers: Pointer[] = [];
|
||||||
|
static resetAll() {
|
||||||
|
Pointer.activePointers.forEach((pointer) => pointer.reset());
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pointerType: "mouse" | "touch" | "pen",
|
private readonly pointerType: "mouse" | "touch" | "pen",
|
||||||
private readonly pointerId = 1,
|
private readonly pointerId = 1,
|
||||||
) {}
|
) {
|
||||||
|
Pointer.activePointers.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.clientX = 0;
|
this.clientX = 0;
|
||||||
|
@ -402,7 +409,10 @@ const proxy = <T extends ExcalidrawElement>(
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Tools that can be used to draw shapes */
|
/** Tools that can be used to draw shapes */
|
||||||
type DrawingToolName = Exclude<ToolType, "lock" | "selection" | "eraser">;
|
type DrawingToolName = Exclude<
|
||||||
|
ToolType,
|
||||||
|
"lock" | "selection" | "eraser" | "lasso"
|
||||||
|
>;
|
||||||
|
|
||||||
type Element<T extends DrawingToolName> = T extends "line" | "freedraw"
|
type Element<T extends DrawingToolName> = T extends "line" | "freedraw"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
|
|
1812
packages/excalidraw/tests/lasso.test.tsx
Normal file
1812
packages/excalidraw/tests/lasso.test.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,5 @@
|
||||||
|
import { newArrowElement } from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
import { pointCenter, pointFrom } from "@excalidraw/math";
|
import { pointCenter, pointFrom } from "@excalidraw/math";
|
||||||
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -19,7 +21,7 @@ import {
|
||||||
import * as textElementUtils from "@excalidraw/element/textElement";
|
import * as textElementUtils from "@excalidraw/element/textElement";
|
||||||
import { wrapText } from "@excalidraw/element/textWrapping";
|
import { wrapText } from "@excalidraw/element/textWrapping";
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -164,6 +166,24 @@ describe("Test Linear Elements", () => {
|
||||||
Keyboard.keyPress(KEYS.DELETE);
|
Keyboard.keyPress(KEYS.DELETE);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it("should normalize the element points at creation", () => {
|
||||||
|
const element = newArrowElement({
|
||||||
|
type: "arrow",
|
||||||
|
points: [pointFrom<LocalPoint>(0.5, 0), pointFrom<LocalPoint>(100, 100)],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
expect(element.points).toEqual([
|
||||||
|
pointFrom<LocalPoint>(0.5, 0),
|
||||||
|
pointFrom<LocalPoint>(100, 100),
|
||||||
|
]);
|
||||||
|
new LinearElementEditor(element);
|
||||||
|
expect(element.points).toEqual([
|
||||||
|
pointFrom<LocalPoint>(0, 0),
|
||||||
|
pointFrom<LocalPoint>(99.5, 100),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not drag line and add midpoint until dragged beyond a threshold", () => {
|
it("should not drag line and add midpoint until dragged beyond a threshold", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import type { AllPossibleKeys } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
||||||
|
|
||||||
import { UI } from "./helpers/ui";
|
import { Pointer, UI } from "./helpers/ui";
|
||||||
import * as toolQueries from "./queries/toolQueries";
|
import * as toolQueries from "./queries/toolQueries";
|
||||||
|
|
||||||
import type { RenderResult, RenderOptions } from "@testing-library/react";
|
import type { RenderResult, RenderOptions } from "@testing-library/react";
|
||||||
|
@ -42,6 +42,10 @@ type TestRenderFn = (
|
||||||
) => Promise<RenderResult<typeof customQueries>>;
|
) => Promise<RenderResult<typeof customQueries>>;
|
||||||
|
|
||||||
const renderApp: TestRenderFn = async (ui, options) => {
|
const renderApp: TestRenderFn = async (ui, options) => {
|
||||||
|
// when tests reuse Pointer instances let's reset the last
|
||||||
|
// pointer poisitions so there's no leak between tests
|
||||||
|
Pointer.resetAll();
|
||||||
|
|
||||||
if (options?.localStorageData) {
|
if (options?.localStorageData) {
|
||||||
initLocalStorage(options.localStorageData);
|
initLocalStorage(options.localStorageData);
|
||||||
delete options.localStorageData;
|
delete options.localStorageData;
|
||||||
|
|
|
@ -136,6 +136,7 @@ export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
||||||
|
|
||||||
export type ToolType =
|
export type ToolType =
|
||||||
| "selection"
|
| "selection"
|
||||||
|
| "lasso"
|
||||||
| "rectangle"
|
| "rectangle"
|
||||||
| "diamond"
|
| "diamond"
|
||||||
| "ellipse"
|
| "ellipse"
|
||||||
|
@ -308,6 +309,8 @@ export interface AppState {
|
||||||
*/
|
*/
|
||||||
lastActiveTool: ActiveTool | null;
|
lastActiveTool: ActiveTool | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
// indicates if the current tool is temporarily switched on from the selection tool
|
||||||
|
fromSelection: boolean;
|
||||||
} & ActiveTool;
|
} & ActiveTool;
|
||||||
penMode: boolean;
|
penMode: boolean;
|
||||||
penDetected: boolean;
|
penDetected: boolean;
|
||||||
|
@ -598,6 +601,7 @@ export interface ExcalidrawProps {
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
aiEnabled?: boolean;
|
aiEnabled?: boolean;
|
||||||
showDeprecatedFonts?: boolean;
|
showDeprecatedFonts?: boolean;
|
||||||
|
renderScrollbars?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
|
@ -721,7 +725,8 @@ export type PointerDownState = Readonly<{
|
||||||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
scrollbars: ReturnType<typeof isOverScrollBars>;
|
||||||
// The previous pointer position
|
// The previous pointer position
|
||||||
lastCoords: { x: number; y: number };
|
lastCoords: { x: number; y: number };
|
||||||
// map of original elements data
|
// original element frozen snapshots so we can access the original
|
||||||
|
// element attribute values at time of pointerdown
|
||||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||||
resize: {
|
resize: {
|
||||||
// Handle when resizing, might change during the pointer interaction
|
// Handle when resizing, might change during the pointer interaction
|
||||||
|
@ -755,6 +760,9 @@ export type PointerDownState = Readonly<{
|
||||||
hasOccurred: boolean;
|
hasOccurred: boolean;
|
||||||
// Might change during the pointer interaction
|
// Might change during the pointer interaction
|
||||||
offset: { x: number; y: number } | null;
|
offset: { x: number; y: number } | null;
|
||||||
|
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||||
|
// to current pointer position at time of duplication.
|
||||||
|
origin: { x: number; y: number };
|
||||||
};
|
};
|
||||||
// We need to have these in the state so that we can unsubscribe them
|
// We need to have these in the state so that we can unsubscribe them
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
mockBoundingClientRect,
|
mockBoundingClientRect,
|
||||||
restoreOriginalGetBoundingClientRect,
|
restoreOriginalGetBoundingClientRect,
|
||||||
} from "../tests/test-utils";
|
} from "../tests/test-utils";
|
||||||
|
import { actionBindText } from "../actions";
|
||||||
|
|
||||||
unmountComponent();
|
unmountComponent();
|
||||||
|
|
||||||
|
@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => {
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
expect(text.text).toBe("Excalidraw");
|
expect(text.text).toBe("Excalidraw");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should reset the text element angle to the container's when binding to rotated non-arrow container", async () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Hello World!",
|
||||||
|
angle: 45,
|
||||||
|
});
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
angle: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle, text]);
|
||||||
|
|
||||||
|
API.setSelectedElements([rectangle, text]);
|
||||||
|
|
||||||
|
h.app.actionManager.executeAction(actionBindText);
|
||||||
|
|
||||||
|
expect(text.angle).toBe(30);
|
||||||
|
expect(rectangle.angle).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset the text element angle to 0 when binding to rotated arrow container", async () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Hello World!",
|
||||||
|
angle: 45,
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
angle: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([arrow, text]);
|
||||||
|
|
||||||
|
API.setSelectedElements([arrow, text]);
|
||||||
|
|
||||||
|
h.app.actionManager.executeAction(actionBindText);
|
||||||
|
|
||||||
|
expect(text.angle).toBe(0);
|
||||||
|
expect(arrow.angle).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep the text label at 0 degrees when used as an arrow label", async () => {
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
angle: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
mouse.doubleClickAt(
|
||||||
|
arrow.x + arrow.width / 2,
|
||||||
|
arrow.y + arrow.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
|
expect(h.elements[1].angle).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep the text label at the same degrees when used as a non-arrow label", async () => {
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
angle: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle]);
|
||||||
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
|
mouse.doubleClickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
|
expect(h.elements[1].angle).toBe(30);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,6 +41,34 @@ export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||||
return inside;
|
return inside;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const polygonIncludesPointNonZero = <Point extends [number, number]>(
|
||||||
|
point: Point,
|
||||||
|
polygon: Point[],
|
||||||
|
): boolean => {
|
||||||
|
const [x, y] = point;
|
||||||
|
let windingNumber = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < polygon.length; i++) {
|
||||||
|
const j = (i + 1) % polygon.length;
|
||||||
|
const [xi, yi] = polygon[i];
|
||||||
|
const [xj, yj] = polygon[j];
|
||||||
|
|
||||||
|
if (yi <= y) {
|
||||||
|
if (yj > y) {
|
||||||
|
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) > 0) {
|
||||||
|
windingNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (yj <= y) {
|
||||||
|
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) < 0) {
|
||||||
|
windingNumber--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return windingNumber !== 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
||||||
p: Point,
|
p: Point,
|
||||||
poly: Polygon<Point>,
|
poly: Polygon<Point>,
|
||||||
|
|
|
@ -160,13 +160,17 @@ export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||||
*/
|
*/
|
||||||
export function lineSegmentIntersectionPoints<
|
export function lineSegmentIntersectionPoints<
|
||||||
Point extends GlobalPoint | LocalPoint,
|
Point extends GlobalPoint | LocalPoint,
|
||||||
>(l: LineSegment<Point>, s: LineSegment<Point>): Point | null {
|
>(
|
||||||
|
l: LineSegment<Point>,
|
||||||
|
s: LineSegment<Point>,
|
||||||
|
threshold?: number,
|
||||||
|
): Point | null {
|
||||||
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
|
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!candidate ||
|
!candidate ||
|
||||||
!pointOnLineSegment(candidate, s) ||
|
!pointOnLineSegment(candidate, s, threshold) ||
|
||||||
!pointOnLineSegment(candidate, l)
|
!pointOnLineSegment(candidate, l, threshold)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,3 +138,5 @@ export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||||
} & {
|
} & {
|
||||||
_brand: "excalimath_ellipse";
|
_brand: "excalimath_ellipse";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
||||||
|
|
|
@ -5,6 +5,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||||
"activeEmbeddable": null,
|
"activeEmbeddable": null,
|
||||||
"activeTool": {
|
"activeTool": {
|
||||||
"customType": null,
|
"customType": null,
|
||||||
|
"fromSelection": false,
|
||||||
"lastActiveTool": null,
|
"lastActiveTool": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
|
|
|
@ -94,11 +94,6 @@ vi.mock(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock("nanoid", () => {
|
|
||||||
return {
|
|
||||||
nanoid: vi.fn(() => "test-id"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
// ReactDOM is located inside index.tsx file
|
// ReactDOM is located inside index.tsx file
|
||||||
// as a result, we need a place for it to render into
|
// as a result, we need a place for it to render into
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
|
|
190
yarn.lock
190
yarn.lock
|
@ -1003,7 +1003,7 @@
|
||||||
"@babel/plugin-transform-modules-commonjs" "^7.25.9"
|
"@babel/plugin-transform-modules-commonjs" "^7.25.9"
|
||||||
"@babel/plugin-transform-typescript" "^7.25.9"
|
"@babel/plugin-transform-typescript" "^7.25.9"
|
||||||
|
|
||||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
|
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
|
||||||
version "7.26.9"
|
version "7.26.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.9.tgz#aa4c6facc65b9cb3f87d75125ffd47781b475433"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.9.tgz#aa4c6facc65b9cb3f87d75125ffd47781b475433"
|
||||||
integrity sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==
|
integrity sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==
|
||||||
|
@ -2220,13 +2220,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
"@radix-ui/primitive@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
|
|
||||||
integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/primitive@1.1.1":
|
"@radix-ui/primitive@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
|
||||||
|
@ -2239,47 +2232,30 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-primitive" "2.0.2"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
|
||||||
"@radix-ui/react-collection@1.0.1":
|
"@radix-ui/react-collection@1.1.2":
|
||||||
version "1.0.1"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.2.tgz#b45eccca1cb902fd078b237316bd9fa81e621e15"
|
||||||
integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==
|
integrity sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-context" "1.1.1"
|
||||||
"@radix-ui/react-context" "1.0.0"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
"@radix-ui/react-slot" "1.0.1"
|
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
|
|
||||||
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@1.1.1":
|
"@radix-ui/react-compose-refs@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
||||||
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
||||||
|
|
||||||
"@radix-ui/react-context@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
|
||||||
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-context@1.1.1":
|
"@radix-ui/react-context@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
|
||||||
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
|
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
|
||||||
|
|
||||||
"@radix-ui/react-direction@1.0.0":
|
"@radix-ui/react-direction@1.1.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc"
|
||||||
integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==
|
integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer@1.1.5":
|
"@radix-ui/react-dismissable-layer@1.1.5":
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
|
@ -2306,14 +2282,6 @@
|
||||||
"@radix-ui/react-primitive" "2.0.2"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-id@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
|
|
||||||
integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-id@1.1.0":
|
"@radix-ui/react-id@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
|
||||||
|
@ -2366,15 +2334,6 @@
|
||||||
"@radix-ui/react-primitive" "2.0.2"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
|
|
||||||
integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.1.2":
|
"@radix-ui/react-presence@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
||||||
|
@ -2383,14 +2342,6 @@
|
||||||
"@radix-ui/react-compose-refs" "1.1.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-primitive@1.0.1":
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
|
|
||||||
integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-slot" "1.0.1"
|
|
||||||
|
|
||||||
"@radix-ui/react-primitive@2.0.2":
|
"@radix-ui/react-primitive@2.0.2":
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
|
||||||
|
@ -2398,29 +2349,20 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-slot" "1.1.2"
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.0.2":
|
"@radix-ui/react-roving-focus@1.1.2":
|
||||||
version "1.0.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz#815d051a54299114a68db6eb8d34c41a3c0a646f"
|
||||||
integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==
|
integrity sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/primitive" "1.0.0"
|
"@radix-ui/react-collection" "1.1.2"
|
||||||
"@radix-ui/react-collection" "1.0.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-context" "1.1.1"
|
||||||
"@radix-ui/react-context" "1.0.0"
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
"@radix-ui/react-direction" "1.0.0"
|
"@radix-ui/react-id" "1.1.0"
|
||||||
"@radix-ui/react-id" "1.0.0"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-slot@1.0.1":
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81"
|
|
||||||
integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-slot@1.1.2":
|
"@radix-ui/react-slot@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
|
@ -2429,41 +2371,25 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-compose-refs" "1.1.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-tabs@1.0.2":
|
"@radix-ui/react-tabs@1.1.3":
|
||||||
version "1.0.2"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz#c47c8202dc676dea47676215863d2ef9b141c17a"
|
||||||
integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==
|
integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/primitive" "1.0.0"
|
"@radix-ui/react-context" "1.1.1"
|
||||||
"@radix-ui/react-context" "1.0.0"
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
"@radix-ui/react-direction" "1.0.0"
|
"@radix-ui/react-id" "1.1.0"
|
||||||
"@radix-ui/react-id" "1.0.0"
|
"@radix-ui/react-presence" "1.1.2"
|
||||||
"@radix-ui/react-presence" "1.0.0"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-roving-focus" "1.1.2"
|
||||||
"@radix-ui/react-roving-focus" "1.0.2"
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
|
|
||||||
integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref@1.1.0":
|
"@radix-ui/react-use-callback-ref@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
|
||||||
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
|
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
|
||||||
|
|
||||||
"@radix-ui/react-use-controllable-state@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
|
|
||||||
integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-use-controllable-state@1.1.0":
|
"@radix-ui/react-use-controllable-state@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
|
||||||
|
@ -2478,13 +2404,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-use-layout-effect@1.0.0":
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
|
|
||||||
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-use-layout-effect@1.1.0":
|
"@radix-ui/react-use-layout-effect@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
||||||
|
@ -8851,16 +8770,8 @@ string-natural-compare@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
name string-width-cjs
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -8962,14 +8873,7 @@ stringify-object@^3.3.0:
|
||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -10102,7 +10006,8 @@ workbox-window@7.3.0, workbox-window@^7.3.0:
|
||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
workbox-core "7.3.0"
|
workbox-core "7.3.0"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
|
name wrap-ansi-cjs
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
@ -10120,15 +10025,6 @@ wrap-ansi@^6.2.0:
|
||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue