Compare commits

...

40 Commits

Author SHA1 Message Date
86605829c6 demo: a temp freehand solution to replace laser 2025-07-21 21:25:02 +10:00
c398af6c92 DISABLE DEBUG 2025-07-15 15:37:18 +02:00
973f2a464d tweak icons 2025-07-15 13:09:37 +02:00
02cef5ea92 Merge branch 'master' into ryan-di/freedraw-width
# Conflicts:
#	packages/excalidraw/package.json
2025-07-15 13:06:50 +02:00
678dff25ed fix: ellipsify MainMenu and CommandPalette items (#9743)
* fix: ellipsify MainMenu and CommandPalette items

* fix lint
2025-07-15 12:59:55 +02:00
0cfa53b764 fix: aligning and distributing elements and nested groups while editing a group (#9721) 2025-07-15 12:43:42 +02:00
d615c2cea1 rename drawingConfigs to freedrawOptions 2025-07-14 13:15:31 +02:00
cde46793f8 feat: support timestamps for youtube video emebds (#9737) 2025-07-13 19:19:10 +02:00
446f871536 Revert "differentiate between constant/variable stroke type"
This reverts commit 0199c82e98.
2025-07-08 23:44:54 +02:00
34bff557e3 tweak icons 2025-07-08 23:42:42 +02:00
a0e54e3768 tweak fixed freedraw stroke width 2025-07-08 23:42:35 +02:00
d6ec1dc7e6 support extraBold for all element types 2025-07-08 23:42:08 +02:00
2d127f8c22 docs: fix broken update scene button example in docs (#9726)
fix: update scene example in docs
2025-07-08 19:29:44 +05:30
4eadb891f8 fix(toast): prevent toast from re-rendering and resetting timeout Fixes #9714 (#9715)
* Update App.tsx

* fix: lint

---------

Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
2025-07-03 17:07:26 +10:00
258605d1d5 chore: release multiple packages (#9698) 2025-06-30 12:19:15 +02:00
62e20aa247 improve debug 2025-06-27 15:43:13 +02:00
0199c82e98 differentiate between constant/variable stroke type 2025-06-27 14:18:48 +02:00
3c07ff358a differentiate freedraw config based on input type 2025-06-27 14:07:12 +02:00
d9c85ff18f bump extraBold width to 8 2025-06-27 13:56:47 +02:00
6d84fa21c5 chore: bump @excalidraw/laser-pointer@1.3.2 2025-06-27 13:39:47 +02:00
5666fd8199 update snap 2025-06-27 20:51:50 +10:00
abdacf8239 code cleanup 2025-06-27 20:36:37 +10:00
1068153b25 merge 2025-06-27 20:26:27 +10:00
09876aba6d change to fixedStrokeWidth 2025-06-27 20:19:32 +10:00
8ceb55dd02 Revert "remove debug and provide value for stylus"
This reverts commit c72c47f0cd.

# Conflicts:
#	packages/element/src/freedraw.ts
#	packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
#	packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
2025-06-26 22:21:47 +02:00
c141500400 chore: Relocate visualdebug so ESLint doesn't complain (#9668) 2025-06-18 14:45:51 +02:00
b1f3cc50ee tweak stroke widths 2025-06-16 22:16:28 +10:00
c72c47f0cd remove debug and provide value for stylus 2025-06-16 17:19:55 +10:00
37b75263f8 put streamline & simplify into ele obj too 2025-06-13 18:12:56 +10:00
c08840358b fix: funky shape corners for freedraw 2025-06-11 18:05:46 +10:00
e99baaa6bb fix simulate pressure 2025-06-09 21:08:57 +10:00
a8857f2849 debug sliders 2025-06-09 17:53:14 +10:00
df1f9281b4 change slider to radio 2025-06-06 00:31:35 +10:00
c210b7b092 improve params and real pressure 2025-06-05 23:00:40 +10:00
660d21fe46 improve freedraw rendering 2025-06-05 16:53:22 +10:00
c7780cb9cb snapshots 2025-06-02 17:33:44 +10:00
4e265629c3 tweak stroke rendering 2025-06-02 17:00:19 +10:00
1c611d6c4f add stroke sensivity action 2025-06-02 16:44:30 +10:00
ab6af41d33 add current item stroke sensivity 2025-06-02 16:43:12 +10:00
15dfe0cc7c add stroke/pressure sensitivity to freedraw 2025-06-02 16:39:42 +10:00
67 changed files with 2064 additions and 477 deletions

View File

@ -24,4 +24,4 @@ jobs:
- name: Auto release
run: |
yarn add @actions/core -W
yarn autorelease
yarn release --tag=next --non-interactive

View File

@ -1,55 +0,0 @@
name: Auto release excalidraw preview
on:
issue_comment:
types: [created, edited]
jobs:
Auto-release-excalidraw-preview:
name: Auto release preview
if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
runs-on: ubuntu-latest
steps:
- name: React to release comment
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
comment-id: ${{ github.event.comment.id }}
reactions: "+1"
- name: Get PR SHA
id: sha
uses: actions/github-script@v4
with:
result-encoding: string
script: |
const { owner, repo, number } = context.issue;
const pr = await github.pulls.get({
owner,
repo,
pull_number: number,
});
return pr.data.head.sha
- uses: actions/checkout@v2
with:
ref: ${{ steps.sha.outputs.result }}
fetch-depth: 2
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release preview
id: "autorelease"
run: |
yarn add @actions/core -W
yarn autorelease preview ${{ github.event.issue.number }}
- name: Post comment post release
if: always()
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
issue-number: ${{ github.event.issue.number }}
body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"

View File

@ -28,32 +28,12 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
## Releasing
### Create a test release
You can create a test release by posting the below comment in your pull request:
```bash
@excalibot trigger release
```
Once the version is released `@excalibot` will post a comment with the release version.
### Creating a production release
To release the next stable version follow the below steps:
```bash
yarn prerelease:excalidraw
yarn release --tag=latest --version=0.19.0
```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
The next step is to run the `release` script:
```bash
yarn release:excalidraw
```
This will publish the package.
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.
You will need to pass the `latest` tag with `version` for which you want to create the release. This will make the changes needed before publishing the packages into NPM, like updating dependencies of all `@excalidraw/*` packages, generating new entries in `CHANGELOG.md` and more.

View File

@ -33,6 +33,7 @@ const ExcalidrawScope = {
initialData,
useI18n: ExcalidrawComp.useI18n,
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
};
export default ExcalidrawScope;

View File

@ -3,7 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
"build:packages": "yarn --cwd ../../ build:packages",
"build:workspace": "yarn build:packages && yarn copy:assets",
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
"dev": "yarn build:workspace && next dev -p 3005",
"build": "yarn build:workspace && next build",

View File

@ -17,6 +17,6 @@
"build": "vite build",
"preview": "vite preview --port 5002",
"build:preview": "yarn build && yarn preview",
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
"build:packages": "yarn --cwd ../../ build:packages"
}
}

View File

@ -1,5 +1,5 @@
{
"outputDirectory": "dist",
"installCommand": "yarn install",
"buildCommand": "yarn build:package && yarn build"
"buildCommand": "yarn build:packages && yarn build"
}

View File

@ -134,6 +134,7 @@ import DebugCanvas, {
} from "./components/DebugCanvas";
import { AIComponents } from "./components/AI";
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
import { FreedrawDebugSliders } from "./components/FreedrawDebugSliders";
import "./index.scss";
@ -1142,6 +1143,7 @@ const ExcalidrawWrapper = () => {
ref={debugCanvasRef}
/>
)}
{/* <FreedrawDebugSliders /> */}
</Excalidraw>
</div>
);

View File

@ -18,10 +18,10 @@ import {
} from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve";
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
import type { Curve } from "@excalidraw/math";
import type { DebugElement } from "@excalidraw/utils/visualdebug";
import { STORAGE_KEYS } from "../app_constants";
const renderLine = (

View File

@ -0,0 +1,150 @@
import { STROKE_OPTIONS, isFreeDrawElement } from "@excalidraw/element";
import { useState, useEffect } from "react";
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
import { useExcalidrawElements } from "@excalidraw/excalidraw/components/App";
import { round } from "../../packages/math/src";
export const FreedrawDebugSliders = () => {
const [streamline, setStreamline] = useState<number>(
STROKE_OPTIONS.default.streamline,
);
const [simplify, setSimplify] = useState<number>(
STROKE_OPTIONS.default.simplify,
);
useEffect(() => {
if (!window.h) {
window.h = {} as any;
}
if (!window.h.debugFreedraw) {
window.h.debugFreedraw = {
enabled: true,
...STROKE_OPTIONS.default,
};
}
setStreamline(window.h.debugFreedraw.streamline);
setSimplify(window.h.debugFreedraw.simplify);
}, []);
const handleStreamlineChange = (value: number) => {
setStreamline(value);
if (window.h && window.h.debugFreedraw) {
window.h.debugFreedraw.streamline = value;
}
};
const handleSimplifyChange = (value: number) => {
setSimplify(value);
if (window.h && window.h.debugFreedraw) {
window.h.debugFreedraw.simplify = value;
}
};
const [enabled, setEnabled] = useState<boolean>(
window.h?.debugFreedraw?.enabled ?? true,
);
// counter incrasing each 50ms
const [, setCounter] = useState<number>(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter((prev) => prev + 1);
}, 50);
return () => clearInterval(interval);
}, []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const elements = useExcalidrawElements();
const appState = useUIAppState();
const newFreedrawElement =
appState.newElement && isFreeDrawElement(appState.newElement)
? appState.newElement
: null;
return (
<div
style={{
position: "absolute",
bottom: "70px",
left: "50%",
transform: "translateX(-50%)",
zIndex: 9999,
padding: "10px",
borderRadius: "8px",
border: "1px solid #ccc",
display: "flex",
flexDirection: "column",
gap: "8px",
fontSize: "12px",
fontFamily: "monospace",
}}
>
{newFreedrawElement && (
<div>
pressures:{" "}
{newFreedrawElement.simulatePressure
? "simulated"
: JSON.stringify(
newFreedrawElement.pressures
.slice(-4)
.map((x) => round(x, 2))
.join(" ") || [],
)}{" "}
({round(window.__lastPressure__ || 0, 2) || "?"})
</div>
)}
<div>
<label>
{" "}
enabled
<br />
<input
type="checkbox"
checked={enabled}
onChange={(e) => {
if (window.h.debugFreedraw) {
window.h.debugFreedraw.enabled = e.target.checked;
setEnabled(e.target.checked);
}
}}
/>
</label>
</div>
<div>
<label>
Streamline: {streamline.toFixed(2)}
<br />
<input
type="range"
min="0"
max="1"
step="0.01"
value={streamline}
onChange={(e) => handleStreamlineChange(parseFloat(e.target.value))}
style={{ width: "150px" }}
/>
</label>
</div>
<div>
<label>
Simplify: {simplify.toFixed(2)}
<br />
<input
type="range"
min="0"
max="1"
step="0.01"
value={simplify}
onChange={(e) => handleSimplifyChange(parseFloat(e.target.value))}
style={{ width: "150px" }}
/>
</label>
</div>
</div>
);
};

View File

@ -1,34 +0,0 @@
import { defaultLang } from "@excalidraw/excalidraw/i18n";
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
import {
screen,
fireEvent,
waitFor,
render,
} from "@excalidraw/excalidraw/tests/test-utils";
import ExcalidrawApp from "../App";
describe("Test LanguageList", () => {
it("rerenders UI on language change", async () => {
await render(<ExcalidrawApp />);
// select rectangle tool to show properties menu
UI.clickTool("rectangle");
// english lang should display `thin` label
expect(screen.queryByTitle(/thin/i)).not.toBeNull();
fireEvent.click(document.querySelector(".dropdown-menu-button")!);
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: "de-DE" },
});
// switching to german, `thin` label should no longer exist
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
// reset language
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: defaultLang.code },
});
// switching back to English
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
});
});

View File

@ -52,13 +52,17 @@
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
"build:app": "yarn --cwd ./excalidraw-app build:app",
"build:package": "yarn --cwd ./packages/excalidraw build:esm",
"build:common": "yarn --cwd ./packages/common build:esm",
"build:element": "yarn --cwd ./packages/element build:esm",
"build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm",
"build:math": "yarn --cwd ./packages/math build:esm",
"build:packages": "yarn build:common && yarn build:math && yarn build:element && yarn build:excalidraw",
"build:version": "yarn --cwd ./excalidraw-app build:version",
"build": "yarn --cwd ./excalidraw-app build",
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
"start": "yarn --cwd ./excalidraw-app start",
"start:production": "yarn --cwd ./excalidraw-app start:production",
"start:example": "yarn build:package && yarn --cwd ./examples/with-script-in-browser start",
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
"test:app": "vitest",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
@ -76,9 +80,10 @@
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"autorelease": "node scripts/autorelease.js",
"prerelease:excalidraw": "node scripts/prerelease.js",
"release:excalidraw": "node scripts/release.js",
"release": "node scripts/release.js",
"release:test": "node scripts/release.js --tag=test",
"release:next": "node scripts/release.js --tag=next",
"release:latest": "node scripts/release.js --tag=latest",
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
"clean-install": "yarn rm:node_modules && yarn install"

View File

@ -1,6 +1,6 @@
{
"name": "@excalidraw/common",
"version": "0.1.0",
"version": "0.18.0",
"type": "module",
"types": "./dist/types/common/src/index.d.ts",
"main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js"
},
"./*": {
"types": "./dist/types/common/src/*.d.ts"
"types": "./dist/types/common/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
}
},
"files": [

View File

@ -415,8 +415,9 @@ export const ROUGHNESS = {
export const STROKE_WIDTH = {
thin: 1,
bold: 2,
extraBold: 4,
medium: 2,
bold: 4,
extraBold: 8,
} as const;
export const DEFAULT_ELEMENT_PROPS: {
@ -432,7 +433,7 @@ export const DEFAULT_ELEMENT_PROPS: {
strokeColor: COLOR_PALETTE.black,
backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "solid",
strokeWidth: 2,
strokeWidth: STROKE_WIDTH.medium,
strokeStyle: "solid",
roughness: ROUGHNESS.artist,
opacity: 100,

View File

@ -1,6 +1,6 @@
{
"name": "@excalidraw/element",
"version": "0.1.0",
"version": "0.18.0",
"type": "module",
"types": "./dist/types/element/src/index.d.ts",
"main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js"
},
"./*": {
"types": "./dist/types/element/src/*.d.ts"
"types": "./dist/types/element/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
}
},
"files": [
@ -52,5 +55,9 @@
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
},
"dependencies": {
"@excalidraw/common": "0.18.0",
"@excalidraw/math": "0.18.0"
}
}

View File

@ -1,6 +1,8 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds";
import { getMaximumGroups } from "./groups";
import { getSelectedElementsByGroup } from "./groups";
import type { Scene } from "./Scene";
@ -16,11 +18,12 @@ export const alignElements = (
selectedElements: ExcalidrawElement[],
alignment: Alignment,
scene: Scene,
appState: Readonly<AppState>,
): ExcalidrawElement[] => {
const elementsMap = scene.getNonDeletedElementsMap();
const groups: ExcalidrawElement[][] = getMaximumGroups(
const groups: ExcalidrawElement[][] = getSelectedElementsByGroup(
selectedElements,
elementsMap,
scene.getNonDeletedElementsMap(),
appState,
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);

View File

@ -5,6 +5,7 @@ import {
invariant,
rescalePoints,
sizeOf,
STROKE_WIDTH,
} from "@excalidraw/common";
import {
@ -808,9 +809,15 @@ export const getArrowheadPoints = (
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
const lengthMultiplier =
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
const minSize = Math.min(size, length * lengthMultiplier);
const xs = x2 - nx * minSize;
const ys = y2 - ny * minSize;
// make arrowheads bigger for thick strokes
const strokeWidthMultiplier =
element.strokeWidth >= STROKE_WIDTH.extraBold ? 1.5 : 1;
const adjustedSize =
Math.min(size, length * lengthMultiplier) * strokeWidthMultiplier;
const xs = x2 - nx * adjustedSize;
const ys = y2 - ny * adjustedSize;
if (
arrowhead === "dot" ||
@ -859,7 +866,7 @@ export const getArrowheadPoints = (
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
[ox, oy] = pointRotateRads(
pointFrom(x2 + minSize * 2, y2),
pointFrom(x2 + adjustedSize * 2, y2),
pointFrom(x2, y2),
Math.atan2(py - y2, px - x2) as Radians,
);
@ -870,7 +877,7 @@ export const getArrowheadPoints = (
: [0, 0];
[ox, oy] = pointRotateRads(
pointFrom(x2 - minSize * 2, y2),
pointFrom(x2 - adjustedSize * 2, y2),
pointFrom(x2, y2),
Math.atan2(y2 - py, x2 - px) as Radians,
);

View File

@ -1,7 +1,9 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { getCommonBoundingBox } from "./bounds";
import { newElementWith } from "./mutateElement";
import { getMaximumGroups } from "./groups";
import { getSelectedElementsByGroup } from "./groups";
import type { ElementsMap, ExcalidrawElement } from "./types";
@ -14,6 +16,7 @@ export const distributeElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
distribution: Distribution,
appState: Readonly<AppState>,
): ExcalidrawElement[] => {
const [start, mid, end, extent] =
distribution.axis === "x"
@ -21,7 +24,11 @@ export const distributeElements = (
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements, elementsMap)
const groups = getSelectedElementsByGroup(
selectedElements,
elementsMap,
appState,
)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.sort((a, b) => a[1][mid] - b[1][mid]);

View File

@ -23,7 +23,7 @@ type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
const embeddedLinkCache = new Map<string, IframeDataWithSandbox>();
const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)/;
const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
@ -56,6 +56,35 @@ const RE_REDDIT =
const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const parseYouTubeTimestamp = (url: string): number => {
let timeParam: string | null | undefined;
try {
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
timeParam =
urlObj.searchParams.get("t") || urlObj.searchParams.get("start");
} catch (error) {
const timeMatch = url.match(/[?&#](?:t|start)=([^&#\s]+)/);
timeParam = timeMatch?.[1];
}
if (!timeParam) {
return 0;
}
if (/^\d+$/.test(timeParam)) {
return parseInt(timeParam, 10);
}
const timeMatch = timeParam.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
if (!timeMatch) {
return 0;
}
const [, hours = "0", minutes = "0", seconds = "0"] = timeMatch;
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
};
const ALLOWED_DOMAINS = new Set([
"youtube.com",
"youtu.be",
@ -113,7 +142,8 @@ export const getEmbedLink = (
let aspectRatio = { w: 560, h: 840 };
const ytLink = link.match(RE_YOUTUBE);
if (ytLink?.[2]) {
const time = ytLink[3] ? `&start=${ytLink[3]}` : ``;
const startTime = parseYouTubeTimestamp(originalLink);
const time = startTime > 0 ? `&start=${startTime}` : ``;
const isPortrait = link.includes("shorts");
type = "video";
switch (ytLink[1]) {

View File

@ -0,0 +1,278 @@
import { LaserPointer, type Point } from "@excalidraw/laser-pointer";
import { clamp, round, type LocalPoint } from "@excalidraw/math";
import getStroke from "perfect-freehand";
import type { StrokeOptions } from "perfect-freehand";
import type { ExcalidrawFreeDrawElement, PointerType } from "./types";
export const STROKE_OPTIONS: Record<
PointerType | "default",
{ streamline: number; simplify: number }
> = {
default: {
streamline: 0.35,
simplify: 0.1,
},
mouse: {
streamline: 0.6,
simplify: 0.1,
},
pen: {
// for optimal performance, we use a lower streamline and simplify
streamline: 0.2,
simplify: 0.1,
},
touch: {
streamline: 0.65,
simplify: 0.1,
},
} as const;
export const getFreedrawConfig = (eventType: string | null | undefined) => {
return (
STROKE_OPTIONS[(eventType as PointerType | null) || "default"] ||
STROKE_OPTIONS.default
);
};
/**
* Calculates simulated pressure based on velocity between consecutive points.
* Fast movement (large distances) -> lower pressure
* Slow movement (small distances) -> higher pressure
*/
const calculateVelocityBasedPressure = (
points: readonly LocalPoint[],
index: number,
fixedStrokeWidth: boolean | undefined,
maxDistance = 8, // Maximum expected distance for normalization
): number => {
if (fixedStrokeWidth) {
return 1;
}
// First point gets highest pressure
// This avoid "a dot followed by a line" effect, •== when first stroke is "slow"
if (index === 0) {
return 1;
}
const [x1, y1] = points[index - 1];
const [x2, y2] = points[index];
// Calculate distance between consecutive points
const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
// Normalize distance and invert for pressure (0 = fast/low pressure, 1 = slow/high pressure)
const normalizedDistance = Math.min(distance / maxDistance, 1);
const basePressure = Math.max(0.1, 1 - normalizedDistance * 0.7); // Range: 0.1 to 1.0
const constantPressure = 0.5;
const pressure = constantPressure + (basePressure - constantPressure);
return Math.max(0.1, Math.min(1.0, pressure));
};
export const getFreedrawStroke = (element: ExcalidrawFreeDrawElement) => {
// Compose points as [x, y, pressure]
let points: [number, number, number][];
if (element.freedrawOptions?.fixedStrokeWidth) {
points = element.points.map(
([x, y]: LocalPoint): [number, number, number] => [x, y, 1],
);
} else if (element.simulatePressure) {
// Simulate pressure based on velocity between consecutive points
points = element.points.map(([x, y]: LocalPoint, i) => [
x,
y,
calculateVelocityBasedPressure(
element.points,
i,
element.freedrawOptions?.fixedStrokeWidth,
),
]);
} else {
points = element.points.map(([x, y]: LocalPoint, i) => {
const rawPressure = element.pressures?.[i] ?? 0.5;
const amplifiedPressure = Math.pow(rawPressure, 0.6);
const adjustedPressure = amplifiedPressure;
return [x, y, clamp(adjustedPressure, 0.1, 1.0)];
});
}
const streamline =
element.freedrawOptions?.streamline ?? STROKE_OPTIONS.default.streamline;
const simplify =
element.freedrawOptions?.simplify ?? STROKE_OPTIONS.default.simplify;
const laser = new LaserPointer({
size: element.strokeWidth,
streamline,
simplify,
sizeMapping: ({ pressure: t }) => {
if (element.freedrawOptions?.fixedStrokeWidth) {
return 0.6;
}
if (element.simulatePressure) {
return 0.2 + t * 0.6;
}
return 0.2 + t * 0.8;
},
});
for (const pt of points) {
laser.addPoint(pt);
}
laser.close();
return laser.getStrokeOutline();
};
/**
* Generates an SVG path for a freedraw element using LaserPointer logic.
* Uses actual pressure data if available, otherwise simulates pressure based on velocity.
* No streamline, smoothing, or simulation is performed.
*/
export const getFreeDrawSvgPath = (
element: ExcalidrawFreeDrawElement,
): string => {
// legacy, for backwards compatibility
if (element.freedrawOptions === null) {
return _legacy_getFreeDrawSvgPath(element);
}
return _transition_getFreeDrawSvgPath(element);
// return getSvgPathFromStroke(getFreedrawStroke(element));
};
const roundPoint = (A: Point): string => {
return `${round(A[0], 4, "round")},${round(A[1], 4, "round")} `;
};
const average = (A: Point, B: Point): string => {
return `${round((A[0] + B[0]) / 2, 4, "round")},${round(
(A[1] + B[1]) / 2,
4,
"round",
)} `;
};
export const getSvgPathFromStroke = (points: Point[]): string => {
const len = points.length;
if (len < 2) {
return "";
}
let a = points[0];
let b = points[1];
if (len === 2) {
return `M${roundPoint(a)}L${roundPoint(b)}`;
}
let result = "";
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += average(a, b);
}
return `M${roundPoint(points[0])}Q${roundPoint(points[1])}${average(
points[1],
points[2],
)}${points.length > 3 ? "T" : ""}${result}L${roundPoint(points[len - 1])}`;
};
function _transition_getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
const inputPoints = element.simulatePressure
? element.points
: element.points.length
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]];
// Consider changing the options for simulated pressure vs real pressure
const options: StrokeOptions = {
simulatePressure: element.simulatePressure,
size: element.strokeWidth,
thinning: 0.6,
smoothing: 0.5,
streamline: 0.5,
easing: (t) => {
if (element.freedrawOptions?.fixedStrokeWidth) {
return 0.5;
}
return Math.sin((t * Math.PI) / 2) * 0.65;
}, // https://easings.net/#easeOutSine
last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
};
return _legacy_getSvgPathFromStroke(
getStroke(inputPoints as number[][], options),
);
}
function _legacy_getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
// If input points are empty (should they ever be?) return a dot
const inputPoints = element.simulatePressure
? element.points
: element.points.length
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]];
// Consider changing the options for simulated pressure vs real pressure
const options: StrokeOptions = {
simulatePressure: element.simulatePressure,
size: element.strokeWidth * 4.25,
thinning: 0.6,
smoothing: 0.5,
streamline: 0.5,
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
};
return _legacy_getSvgPathFromStroke(
getStroke(inputPoints as number[][], options),
);
}
const med = (A: number[], B: number[]) => {
return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2];
};
// Trim SVG path data so number are each two decimal points. This
// improves SVG exports, and prevents rendering errors on points
// with long decimals.
const TO_FIXED_PRECISION = /(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g;
const _legacy_getSvgPathFromStroke = (points: number[][]): string => {
if (!points.length) {
return "";
}
const max = points.length - 1;
return points
.reduce(
(acc, point, i, arr) => {
if (i === max) {
acc.push(point, med(point, arr[0]), "L", arr[0], "Z");
} else {
acc.push(point, med(point, arr[i + 1]));
}
return acc;
},
["M", points[0], "Q"],
)
.join(" ")
.replace(TO_FIXED_PRECISION, "$1");
};

View File

@ -7,6 +7,8 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { getBoundTextElement } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
import type {
@ -402,3 +404,78 @@ export const getNewGroupIdsForDuplication = (
return copy;
};
// given a list of selected elements, return the element grouped by their immediate group selected state
// in the case if only one group is selected and all elements selected are within the group, it will respect group hierarchy in accordance to their nested grouping order
export const getSelectedElementsByGroup = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
appState: Readonly<AppState>,
): ExcalidrawElement[][] => {
const selectedGroupIds = getSelectedGroupIds(appState);
const unboundElements = selectedElements.filter(
(element) => !isBoundToContainer(element),
);
const groups: Map<string, ExcalidrawElement[]> = new Map();
const elements: Map<string, ExcalidrawElement[]> = new Map();
// helper function to add an element to the elements map
const addToElementsMap = (element: ExcalidrawElement) => {
// elements
const currentElementMembers = elements.get(element.id) || [];
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
currentElementMembers.push(boundTextElement);
}
elements.set(element.id, [...currentElementMembers, element]);
};
// helper function to add an element to the groups map
const addToGroupsMap = (element: ExcalidrawElement, groupId: string) => {
// groups
const currentGroupMembers = groups.get(groupId) || [];
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
currentGroupMembers.push(boundTextElement);
}
groups.set(groupId, [...currentGroupMembers, element]);
};
// helper function to handle the case where a single group is selected
// and all elements selected are within the group, it will respect group hierarchy in accordance to
// their nested grouping order
const handleSingleSelectedGroupCase = (
element: ExcalidrawElement,
selectedGroupId: GroupId,
) => {
const indexOfSelectedGroupId = element.groupIds.indexOf(selectedGroupId, 0);
const nestedGroupCount = element.groupIds.slice(
0,
indexOfSelectedGroupId,
).length;
return nestedGroupCount > 0
? addToGroupsMap(element, element.groupIds[indexOfSelectedGroupId - 1])
: addToElementsMap(element);
};
const isAllInSameGroup = selectedElements.every((element) =>
isSelectedViaGroup(appState, element),
);
unboundElements.forEach((element) => {
const selectedGroupId = getSelectedGroupIdForElement(
element,
appState.selectedGroupIds,
);
if (!selectedGroupId) {
addToElementsMap(element);
} else if (selectedGroupIds.length === 1 && isAllInSameGroup) {
handleSingleSelectedGroupCase(element, selectedGroupId);
} else {
addToGroupsMap(element, selectedGroupId);
}
});
return Array.from(groups.values()).concat(Array.from(elements.values()));
};

View File

@ -91,6 +91,7 @@ export * from "./embeddable";
export * from "./flowchart";
export * from "./fractionalIndex";
export * from "./frame";
export * from "./freedraw";
export * from "./groups";
export * from "./heading";
export * from "./image";

View File

@ -445,6 +445,7 @@ export const newFreeDrawElement = (
points?: ExcalidrawFreeDrawElement["points"];
simulatePressure: boolean;
pressures?: ExcalidrawFreeDrawElement["pressures"];
strokeOptions?: ExcalidrawFreeDrawElement["freedrawOptions"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawFreeDrawElement> => {
return {
@ -453,6 +454,11 @@ export const newFreeDrawElement = (
pressures: opts.pressures || [],
simulatePressure: opts.simulatePressure,
lastCommittedPoint: null,
freedrawOptions: opts.strokeOptions || {
fixedStrokeWidth: true,
streamline: 0.25,
simplify: 0.1,
},
};
};

View File

@ -1,5 +1,4 @@
import rough from "roughjs/bin/rough";
import { getStroke } from "perfect-freehand";
import { isRightAngleRads } from "@excalidraw/math";
@ -58,6 +57,8 @@ import { getCornerRadius } from "./utils";
import { ShapeCache } from "./shape";
import { getFreeDrawSvgPath } from "./freedraw";
import type {
ExcalidrawElement,
ExcalidrawTextElement,
@ -70,7 +71,6 @@ import type {
ElementsMap,
} from "./types";
import type { StrokeOptions } from "perfect-freehand";
import type { RoughCanvas } from "roughjs/bin/canvas";
// using a stronger invert (100% vs our regular 93%) and saturate
@ -1032,57 +1032,3 @@ export function generateFreeDrawShape(element: ExcalidrawFreeDrawElement) {
export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
return pathsCache.get(element);
}
export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
// If input points are empty (should they ever be?) return a dot
const inputPoints = element.simulatePressure
? element.points
: element.points.length
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]];
// Consider changing the options for simulated pressure vs real pressure
const options: StrokeOptions = {
simulatePressure: element.simulatePressure,
size: element.strokeWidth * 4.25,
thinning: 0.6,
smoothing: 0.5,
streamline: 0.5,
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
};
return getSvgPathFromStroke(getStroke(inputPoints as number[][], options));
}
function med(A: number[], B: number[]) {
return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2];
}
// Trim SVG path data so number are each two decimal points. This
// improves SVG exports, and prevents rendering errors on points
// with long decimals.
const TO_FIXED_PRECISION = /(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g;
function getSvgPathFromStroke(points: number[][]): string {
if (!points.length) {
return "";
}
const max = points.length - 1;
return points
.reduce(
(acc, point, i, arr) => {
if (i === max) {
acc.push(point, med(point, arr[0]), "L", arr[0], "Z");
} else {
acc.push(point, med(point, arr[i + 1]));
}
return acc;
},
["M", points[0], "Q"],
)
.join(" ")
.replace(TO_FIXED_PRECISION, "$1");
}

View File

@ -21,6 +21,7 @@ import {
assertNever,
COLOR_PALETTE,
LINE_POLYGON_POINT_MERGE_DISTANCE,
STROKE_WIDTH,
} from "@excalidraw/common";
import { RoughGenerator } from "roughjs/bin/generator";
@ -202,7 +203,7 @@ export const generateRoughOptions = (
// hachureGap because if not specified, roughjs uses strokeWidth to
// calculate them (and we don't want the fills to be modified)
fillWeight: element.strokeWidth / 2,
hachureGap: element.strokeWidth * 4,
hachureGap: Math.min(element.strokeWidth, STROKE_WIDTH.bold) * 4,
roughness: adjustRoughness(element),
stroke: element.strokeColor,
preserveVertices:
@ -806,15 +807,21 @@ const generateElementShape = (
generateFreeDrawShape(element);
if (isPathALoop(element.points)) {
// generate rough polygon to fill freedraw shape
const simplifiedPoints = simplify(
element.points as Mutable<LocalPoint[]>,
0.75,
);
shape = generator.curve(simplifiedPoints as [number, number][], {
...generateRoughOptions(element),
stroke: "none",
});
const points =
element.freedrawOptions === null
? simplify(element.points as LocalPoint[], 0.75)
: simplify(element.points as LocalPoint[], 1.5);
shape =
element.freedrawOptions === null
? generator.curve(points, {
...generateRoughOptions(element),
stroke: "none",
})
: generator.polygon(points, {
...generateRoughOptions(element),
stroke: "none",
});
} else {
shape = null;
}

View File

@ -380,6 +380,11 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
pressures: readonly number[];
simulatePressure: boolean;
lastCommittedPoint: LocalPoint | null;
freedrawOptions: {
streamline?: number;
simplify?: number;
fixedStrokeWidth?: boolean;
} | null;
}>;
export type FileId = string & { _brand: "FileId" };

View File

@ -589,4 +589,424 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(250);
expect(API.getSelectedElements()[3].x).toEqual(150);
});
const createGroupAndSelectInEditGroupMode = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
mouse.moveTo(10, 0);
mouse.doubleClick();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
mouse.moveTo(100, 100);
mouse.click();
});
};
it("aligns elements within a group while in group edit mode correctly to the top", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns elements within a group while in group edit mode correctly to the bottom", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(100);
});
it("aligns elements within a group while in group edit mode correctly to the left", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
});
it("aligns elements within a group while in group edit mode correctly to the right", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(100);
});
it("aligns elements within a group while in group edit mode correctly to the vertical center", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(50);
});
it("aligns elements within a group while in group edit mode correctly to the horizontal center", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(50);
});
const createNestedGroupAndSelectInEditGroupMode = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
mouse.moveTo(200, 200);
// create third element
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// third element is already selected, select the initial group and group together
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
// double click to enter edit mode
mouse.doubleClick();
// select nested group and other element within the group
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(200, 200);
mouse.click();
});
};
it("aligns element and nested group while in group edit mode correctly to the top", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns element and nested group while in group edit mode correctly to the bottom", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns element and nested group while in group edit mode correctly to the left", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns element and nested group while in group edit mode correctly to the right", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("aligns element and nested group while in group edit mode correctly to the vertical center", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("aligns elements and nested group within a group while in group edit mode correctly to the horizontal center", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
const createAndSelectSingleGroup = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
};
it("aligns elements within a single-selected group correctly to the top", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns elements within a single-selected group correctly to the bottom", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(100);
});
it("aligns elements within a single-selected group correctly to the left", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
});
it("aligns elements within a single-selected group correctly to the right", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(100);
});
it("aligns elements within a single-selected group correctly to the vertical center", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(50);
});
it("aligns elements within a single-selected group correctly to the horizontal center", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(50);
});
const createAndSelectSingleGroupWithNestedGroup = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add group to current selection
mouse.restorePosition(10, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create the nested group
API.executeAction(actionGroup);
};
it("aligns elements within a single-selected group containing a nested group correctly to the top", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns elements within a single-selected group containing a nested group correctly to the bottom", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns elements within a single-selected group containing a nested group correctly to the left", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns elements within a single-selected group containing a nested group correctly to the right", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("aligns elements within a single-selected group containing a nested group correctly to the vertical center", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("aligns elements within a single-selected group containing a nested group correctly to the horizontal center", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
});

View File

@ -0,0 +1,153 @@
import { getEmbedLink } from "../src/embeddable";
describe("YouTube timestamp parsing", () => {
it("should parse YouTube URLs with timestamp in seconds", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90",
expectedStart: 90,
},
{
url: "https://youtu.be/dQw4w9WgXcQ?t=120",
expectedStart: 120,
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&start=150",
expectedStart: 150,
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain(`start=${expectedStart}`);
}
});
});
it("should parse YouTube URLs with timestamp in time format", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1m30s",
expectedStart: 90, // 1*60 + 30
},
{
url: "https://youtu.be/dQw4w9WgXcQ?t=2m45s",
expectedStart: 165, // 2*60 + 45
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1h2m3s",
expectedStart: 3723, // 1*3600 + 2*60 + 3
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=45s",
expectedStart: 45,
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=5m",
expectedStart: 300, // 5*60
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=2h",
expectedStart: 7200, // 2*3600
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain(`start=${expectedStart}`);
}
});
});
it("should handle YouTube URLs without timestamps", () => {
const testCases = [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://youtu.be/dQw4w9WgXcQ",
"https://www.youtube.com/embed/dQw4w9WgXcQ",
];
testCases.forEach((url) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).not.toContain("start=");
}
});
});
it("should handle YouTube shorts URLs with timestamps", () => {
const url = "https://www.youtube.com/shorts/dQw4w9WgXcQ?t=30";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=30");
}
// Shorts should have portrait aspect ratio
expect(result?.intrinsicSize).toEqual({ w: 315, h: 560 });
});
it("should handle playlist URLs with timestamps", () => {
const url =
"https://www.youtube.com/playlist?list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ&t=60";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=60");
expect(result.link).toContain("list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ");
}
});
it("should handle malformed or edge case timestamps", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=abc",
expectedStart: 0, // Invalid timestamp should default to 0
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=",
expectedStart: 0, // Empty timestamp should default to 0
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=0",
expectedStart: 0, // Zero timestamp should be handled
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
if (expectedStart === 0) {
expect(result.link).not.toContain("start=");
} else {
expect(result.link).toContain(`start=${expectedStart}`);
}
}
});
});
it("should preserve other URL parameters", () => {
const url =
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90&feature=youtu.be&list=PLtest";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=90");
expect(result.link).toContain("enablejsapi=1");
}
});
});

View File

@ -10,6 +10,8 @@ import { alignElements } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element";
import { getSelectedElementsByGroup } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { Alignment } from "@excalidraw/element";
@ -38,7 +40,11 @@ export const alignActionsPredicate = (
) => {
const selectedElements = app.scene.getSelectedElements(appState);
return (
selectedElements.length > 1 &&
getSelectedElementsByGroup(
selectedElements,
app.scene.getNonDeletedElementsMap(),
appState as Readonly<AppState>,
).length > 1 &&
// TODO enable aligning frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el))
);
@ -52,7 +58,12 @@ const alignSelectedElements = (
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const updatedElements = alignElements(selectedElements, alignment, app.scene);
const updatedElements = alignElements(
selectedElements,
alignment,
app.scene,
appState,
);
const updatedElementsMap = arrayToMap(updatedElements);

View File

@ -10,6 +10,8 @@ import { distributeElements } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element";
import { getSelectedElementsByGroup } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { Distribution } from "@excalidraw/element";
@ -31,7 +33,11 @@ import type { AppClassProperties, AppState } from "../types";
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState);
return (
selectedElements.length > 1 &&
getSelectedElementsByGroup(
selectedElements,
app.scene.getNonDeletedElementsMap(),
appState as Readonly<AppState>,
).length > 2 &&
// TODO enable distributing frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el))
);
@ -49,6 +55,7 @@ const distributeSelectedElements = (
selectedElements,
app.scene.getNonDeletedElementsMap(),
distribution,
appState,
);
const updatedElementsMap = arrayToMap(updatedElements);

View File

@ -145,26 +145,27 @@ describe("element locking", () => {
queryByTestId(document.body, `strokeWidth-thin`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-bold`),
queryByTestId(document.body, `strokeWidth-medium`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-extraBold`),
queryByTestId(document.body, `strokeWidth-bold`),
).not.toBeChecked();
});
it("should show properties of different element types when selected", () => {
const rect = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.bold,
strokeWidth: STROKE_WIDTH.medium,
});
const text = API.createElement({
type: "text",
fontFamily: FONT_FAMILY["Comic Shanns"],
strokeWidth: undefined,
});
API.setElements([rect, text]);
API.setSelectedElements([rect, text]);
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
expect(queryByTestId(document.body, `strokeWidth-medium`)).toBeChecked();
expect(queryByTestId(document.body, `font-family-code`)).toHaveClass(
"active",
);

View File

@ -44,6 +44,7 @@ import {
isArrowElement,
isBoundToContainer,
isElbowArrow,
isFreeDrawElement,
isLinearElement,
isLineElement,
isTextElement,
@ -126,6 +127,9 @@ import {
ArrowheadCrowfootIcon,
ArrowheadCrowfootOneIcon,
ArrowheadCrowfootOneOrManyIcon,
strokeWidthFixedIcon,
strokeWidthVariableIcon,
StrokeWidthMediumIcon,
} from "../components/icons";
import { Fonts } from "../fonts";
@ -503,6 +507,33 @@ export const actionChangeFillStyle = register({
},
});
const WIDTHS = [
{
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
},
{
value: STROKE_WIDTH.medium,
text: t("labels.medium"),
icon: StrokeWidthMediumIcon,
testId: "strokeWidth-medium",
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
},
];
export const actionChangeStrokeWidth = register({
name: "changeStrokeWidth",
label: "labels.strokeWidth",
@ -524,26 +555,7 @@ export const actionChangeStrokeWidth = register({
<div className="buttonList">
<RadioSelection
group="stroke-width"
options={[
{
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
},
]}
options={WIDTHS}
value={getFormValue(
elements,
app,
@ -666,6 +678,70 @@ export const actionChangeStrokeStyle = register({
),
});
export const actionChangePressureSensitivity = register({
name: "changeStrokeType",
label: "labels.strokeType",
trackEvent: false,
perform: (elements, appState, value) => {
const updatedElements = changeProperty(elements, appState, (el) => {
if (isFreeDrawElement(el)) {
return newElementWith(el, {
freedrawOptions: {
...el.freedrawOptions,
fixedStrokeWidth: value,
},
});
}
return el;
});
return {
elements: updatedElements,
appState: { ...appState, currentItemFixedStrokeWidth: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
PanelComponent: ({ app, appState, updateData }) => {
const selectedElements = app.scene.getSelectedElements(app.state);
const freedraws = selectedElements.filter(isFreeDrawElement);
const currentValue =
freedraws.length > 0
? reduceToCommonValue(
freedraws,
(element) => element.freedrawOptions?.fixedStrokeWidth,
) ?? null
: appState.currentItemFixedStrokeWidth;
return (
<fieldset>
<legend>{t("labels.strokeType")}</legend>
<div className="buttonList">
<RadioSelection
group="pressure-sensitivity"
options={[
{
value: true,
text: t("labels.strokeWidthFixed"),
icon: strokeWidthFixedIcon,
testId: "pressure-fixed",
},
{
value: false,
text: t("labels.strokeWidthVariable"),
icon: strokeWidthVariableIcon,
testId: "pressure-variable",
},
]}
value={currentValue}
onChange={(value) => updateData(value)}
/>
</div>
</fieldset>
);
},
});
export const actionChangeOpacity = register({
name: "changeOpacity",
label: "labels.opacity",

View File

@ -13,6 +13,7 @@ export {
actionChangeStrokeWidth,
actionChangeFillStyle,
actionChangeSloppiness,
actionChangePressureSensitivity,
actionChangeOpacity,
actionChangeFontSize,
actionChangeFontFamily,

View File

@ -69,6 +69,7 @@ export type ActionName =
| "changeStrokeStyle"
| "changeArrowhead"
| "changeArrowType"
| "changeStrokeType"
| "changeOpacity"
| "changeFontSize"
| "toggleCanvasMenu"

View File

@ -34,6 +34,7 @@ export const getDefaultAppState = (): Omit<
currentItemFontFamily: DEFAULT_FONT_FAMILY,
currentItemFontSize: DEFAULT_FONT_SIZE,
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
currentItemFixedStrokeWidth: true,
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
currentItemStartArrowhead: null,
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
@ -163,6 +164,11 @@ const APP_STATE_STORAGE_CONF = (<
server: false,
},
currentItemOpacity: { browser: true, export: false, server: false },
currentItemFixedStrokeWidth: {
browser: true,
export: false,
server: false,
},
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },

View File

@ -169,8 +169,12 @@ export const SelectedShapeActions = ({
renderAction("changeStrokeWidth")}
{(appState.activeTool.type === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")}
targetElements.some((element) => element.type === "freedraw")) && (
<>
{renderAction("changeStrokeShape")}
{renderAction("changeStrokeType")}
</>
)}
{(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && (

View File

@ -232,6 +232,8 @@ import {
hitElementBoundingBox,
isLineElement,
isSimpleArrow,
STROKE_OPTIONS,
getFreedrawConfig,
} from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math";
@ -593,6 +595,10 @@ class App extends React.Component<AppProps, AppState> {
* insert to DOM before user initially scrolls to them) */
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
private handleToastClose = () => {
this.setToast(null);
};
private elementsPendingErasure: ElementsPendingErasure = new Set();
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
@ -1707,14 +1713,16 @@ class App extends React.Component<AppProps, AppState> {
/>
</ElementCanvasButtons>
)}
{this.state.toast !== null && (
<Toast
message={this.state.toast.message}
onClose={() => this.setToast(null)}
onClose={this.handleToastClose}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
/>
)}
{this.state.contextMenu && (
<ContextMenu
items={this.state.contextMenu.items}
@ -7472,7 +7480,12 @@ class App extends React.Component<AppProps, AppState> {
y: gridY,
});
const simulatePressure = event.pressure === 0.5;
const simulatePressure =
event.pressure === 0.5 || event.pressure === 0 || event.pressure === 1;
window.__lastPressure__ = event.pressure;
const freedrawConfig = getFreedrawConfig(event.pointerType);
const element = newFreeDrawElement({
type: elementType,
@ -7487,6 +7500,17 @@ class App extends React.Component<AppProps, AppState> {
opacity: this.state.currentItemOpacity,
roundness: null,
simulatePressure,
strokeOptions: {
fixedStrokeWidth: this.state.currentItemFixedStrokeWidth,
streamline:
(window.h?.debugFreedraw?.enabled
? window.h?.debugFreedraw?.streamline
: null) ?? freedrawConfig.streamline,
simplify:
(window.h?.debugFreedraw?.enabled
? window.h?.debugFreedraw?.simplify
: null) ?? freedrawConfig.simplify,
},
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
points: [pointFrom<LocalPoint>(0, 0)],
@ -11128,6 +11152,7 @@ class App extends React.Component<AppProps, AppState> {
// -----------------------------------------------------------------------------
declare global {
interface Window {
__lastPressure__?: number;
h: {
scene: Scene;
elements: readonly ExcalidrawElement[];
@ -11136,6 +11161,11 @@ declare global {
app: InstanceType<typeof App>;
history: History;
store: Store;
debugFreedraw?: {
streamline: number;
simplify: number;
enabled: boolean;
};
};
}
}
@ -11144,6 +11174,12 @@ export const createTestHook = () => {
if (isTestEnv() || isDevEnv()) {
window.h = window.h || ({} as Window["h"]);
// Initialize debug freedraw parameters
window.h.debugFreedraw = {
enabled: true,
...(window.h.debugFreedraw || STROKE_OPTIONS.default),
};
Object.defineProperties(window.h, {
elements: {
configurable: true,

View File

@ -108,6 +108,7 @@ $verticalBreakpoint: 861px;
display: flex;
align-items: center;
gap: 0.25rem;
overflow: hidden;
}
}

View File

@ -59,6 +59,8 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { useStable } from "../../hooks/useStable";
import { Ellipsify } from "../Ellipsify";
import * as defaultItems from "./defaultCommandPaletteItems";
import "./CommandPalette.scss";
@ -964,7 +966,7 @@ const CommandItem = ({
}
/>
)}
{command.label}
<Ellipsify>{command.label}</Ellipsify>
</div>
{showShortcut && command.shortcut && (
<CommandShortcutHint shortcut={command.shortcut} />

View File

@ -0,0 +1,18 @@
export const Ellipsify = ({
children,
...rest
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
{...rest}
style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
...rest.style,
}}
>
{children}
</span>
);
};

View File

@ -7,6 +7,7 @@ export const InlineIcon = ({ icon }: { icon: React.ReactNode }) => {
display: "inline-block",
lineHeight: 0,
verticalAlign: "middle",
flex: "0 0 auto",
}}
>
{icon}

View File

@ -19,6 +19,8 @@
border-radius: var(--border-radius-lg);
position: relative;
transition: box-shadow 0.5s ease-in-out;
display: flex;
flex-direction: column;
&.zen-mode {
box-shadow: none;
@ -100,6 +102,7 @@
align-items: center;
cursor: pointer;
border-radius: var(--border-radius-md);
flex: 1 0 auto;
@media screen and (min-width: 1921px) {
height: 2.25rem;

View File

@ -1,5 +1,7 @@
import { useDevice } from "../App";
import { Ellipsify } from "../Ellipsify";
import type { JSX } from "react";
const MenuItemContent = ({
@ -18,7 +20,7 @@ const MenuItemContent = ({
<>
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
<div style={textStyle} className="dropdown-menu-item__text">
{children}
<Ellipsify>{children}</Ellipsify>
</div>
{shortcut && !device.editor.isMobile && (
<div className="dropdown-menu-item__shortcut">{shortcut}</div>

View File

@ -1136,7 +1136,7 @@ export const StrokeWidthBaseIcon = createIcon(
modifiedTablerIconProps,
);
export const StrokeWidthBoldIcon = createIcon(
export const StrokeWidthMediumIcon = createIcon(
<path
d="M5 10h10"
stroke="currentColor"
@ -1147,7 +1147,7 @@ export const StrokeWidthBoldIcon = createIcon(
modifiedTablerIconProps,
);
export const StrokeWidthExtraBoldIcon = createIcon(
export const StrokeWidthBoldIcon = createIcon(
<path
d="M5 10h10"
stroke="currentColor"
@ -1158,6 +1158,17 @@ export const StrokeWidthExtraBoldIcon = createIcon(
modifiedTablerIconProps,
);
export const StrokeWidthExtraBoldIcon = createIcon(
<path
d="M5 10h10"
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>,
modifiedTablerIconProps,
);
export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
createIcon(
<path
@ -2269,3 +2280,71 @@ export const elementLinkIcon = createIcon(
</g>,
tablerIconProps,
);
export const strokeWidthFixedIcon = createIcon(
<g>
<path
d="M4 12 C 5 8, 6 8, 8 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 12 C 9 16, 10 16, 12 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 12 C 14 8, 15 8, 16 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 12 C 17 16, 18 16, 19 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>,
tablerIconProps,
);
export const strokeWidthVariableIcon = createIcon(
<g>
<path
d="M4 12 C 5 8, 6 8, 8 12"
fill="none"
stroke-width="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 12 C 9 16, 10 16, 12 12"
fill="none"
stroke-width="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 12 C 14 8, 15 8, 16 12"
fill="none"
stroke-width="2.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 12 C 17 16, 18 16, 19 12"
fill="none"
stroke-width="3.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>,
tablerIconProps,
);

View File

@ -302,6 +302,8 @@ const restoreElement = (
lastCommittedPoint: null,
simulatePressure: element.simulatePressure,
pressures: element.pressures,
// legacy, for backwards compatibility
freedrawOptions: element.freedrawOptions ?? null,
});
}
case "image":

View File

@ -248,7 +248,7 @@ export {
loadSceneOrLibraryFromBlob,
loadLibraryFromBlob,
} from "./data/blob";
export { getFreeDrawSvgPath } from "@excalidraw/element";
export { getFreeDrawSvgPath } from "@excalidraw/element/freedraw";
export { mergeLibraryItems, getLibraryItemsHash } from "./data/library";
export { isLinearElement } from "@excalidraw/element";
@ -281,6 +281,7 @@ export { Sidebar } from "./components/Sidebar/Sidebar";
export { Button } from "./components/Button";
export { Footer };
export { MainMenu };
export { Ellipsify } from "./components/Ellipsify";
export { useDevice } from "./components/App";
export { WelcomeScreen };
export { LiveCollaborationTrigger };

View File

@ -32,6 +32,9 @@
"strokeStyle_dotted": "Dotted",
"sloppiness": "Sloppiness",
"opacity": "Opacity",
"strokeType": "Stroke Type",
"strokeWidthFixed": "Fixed width",
"strokeWidthVariable": "Variable width",
"textAlign": "Text align",
"edges": "Edges",
"sharp": "Sharp",

View File

@ -66,13 +66,23 @@
"last 1 safari version"
]
},
"repository": "https://github.com/excalidraw/excalidraw",
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
},
"peerDependencies": {
"react": "^17.0.2 || ^18.2.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
},
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/common": "0.18.0",
"@excalidraw/element": "0.18.0",
"@excalidraw/math": "0.18.0",
"@excalidraw/laser-pointer": "1.3.2",
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/random-username": "1.1.0",
"@radix-ui/react-popover": "1.1.6",
@ -124,12 +134,5 @@
"harfbuzzjs": "0.3.6",
"jest-diff": "29.7.0",
"typescript": "4.9.4"
},
"repository": "https://github.com/excalidraw/excalidraw",
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
}
}

View File

@ -894,6 +894,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1092,6 +1093,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1305,6 +1307,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1635,6 +1638,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1965,6 +1969,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2178,6 +2183,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2418,6 +2424,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2715,6 +2722,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3086,6 +3094,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"currentItemBackgroundColor": "#a5d8ff",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "cross-hatch",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 60,
@ -3094,7 +3103,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#e03131",
"currentItemStrokeStyle": "dotted",
"currentItemStrokeWidth": 2,
"currentItemStrokeWidth": 4,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@ -3205,11 +3214,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"seed": 449462985,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 2,
"strokeWidth": 4,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1359939303,
"versionNonce": 2004587015,
"width": 20,
"x": -10,
"y": 0,
@ -3234,14 +3243,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 60,
"roughness": 2,
"roundness": null,
"seed": 640725609,
"seed": 941653321,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 2,
"strokeWidth": 4,
"type": "rectangle",
"updated": 1,
"version": 9,
"versionNonce": 908564423,
"version": 10,
"versionNonce": 1359939303,
"width": 20,
"x": 20,
"y": 30,
@ -3250,7 +3259,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`;
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `16`;
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `17`;
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] redo stack 1`] = `[]`;
@ -3450,11 +3459,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"strokeStyle": "dotted",
"strokeWidth": 4,
"version": 7,
},
"inserted": {
"strokeStyle": "solid",
"strokeWidth": 2,
"version": 6,
},
},
@ -3475,11 +3484,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"roughness": 2,
"strokeStyle": "dotted",
"version": 8,
},
"inserted": {
"roughness": 1,
"strokeStyle": "solid",
"version": 7,
},
},
@ -3500,11 +3509,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"opacity": 60,
"roughness": 2,
"version": 9,
},
"inserted": {
"opacity": 100,
"roughness": 1,
"version": 8,
},
},
@ -3512,6 +3521,31 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
"id": "id17",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id3": {
"deleted": {
"opacity": 60,
"version": 10,
},
"inserted": {
"opacity": 100,
"version": 9,
},
},
},
},
"id": "id19",
},
{
"appState": AppStateDelta {
"delta": Delta {
@ -3539,6 +3573,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 2,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 4,
"version": 4,
},
"inserted": {
@ -3548,12 +3583,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"version": 3,
},
},
},
},
"id": "id19",
"id": "id21",
},
]
`;
@ -3578,6 +3614,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3900,6 +3937,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4222,6 +4260,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5506,6 +5545,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6722,6 +6762,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7656,6 +7697,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8655,6 +8697,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9645,6 +9688,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,

View File

@ -15,7 +15,11 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
Click me
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Click me
</span>
</div>
</button>
<a
@ -27,7 +31,11 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
Excalidraw blog
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Excalidraw blog
</span>
</div>
</a>
<div
@ -88,7 +96,11 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
Help
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Help
</span>
</div>
<div
class="dropdown-menu-item__shortcut"
@ -138,7 +150,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Open
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Open
</span>
</div>
<div
class="dropdown-menu-item__shortcut"
@ -175,7 +191,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Save to...
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Save to...
</span>
</div>
</button>
<button
@ -231,7 +251,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Export image...
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Export image...
</span>
</div>
<div
class="dropdown-menu-item__shortcut"
@ -280,7 +304,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Find on canvas
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Find on canvas
</span>
</div>
<div
class="dropdown-menu-item__shortcut"
@ -337,7 +365,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Help
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Help
</span>
</div>
<div
class="dropdown-menu-item__shortcut"
@ -374,7 +406,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Reset the canvas
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Reset the canvas
</span>
</div>
</button>
<div
@ -419,7 +455,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
GitHub
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
GitHub
</span>
</div>
</a>
<a
@ -465,7 +505,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Follow us
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Follow us
</span>
</div>
</a>
<a
@ -505,7 +549,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Discord chat
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Discord chat
</span>
</div>
</a>
</div>
@ -542,7 +590,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
Dark mode
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Dark mode
</span>
</div>
<div
class="dropdown-menu-item__shortcut"

View File

@ -20,6 +20,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -632,6 +633,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1114,6 +1116,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1477,6 +1480,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1843,6 +1847,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2105,6 +2110,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2543,6 +2549,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2804,6 +2811,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3069,6 +3077,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3362,6 +3371,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3647,6 +3657,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3881,6 +3892,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4137,6 +4149,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4407,6 +4420,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4635,6 +4649,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4863,6 +4878,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5089,6 +5105,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5315,6 +5332,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5570,6 +5588,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5831,6 +5850,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6193,6 +6213,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6566,6 +6587,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6877,6 +6899,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7179,6 +7202,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7376,6 +7400,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7727,6 +7752,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8078,6 +8104,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8483,6 +8510,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8580,6 +8608,11 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -8612,15 +8645,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
50,
],
],
"pressures": [
0,
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -8687,6 +8715,11 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -8718,15 +8751,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
50,
],
],
"pressures": [
0,
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -8769,6 +8797,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9032,6 +9061,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9296,6 +9326,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9527,6 +9558,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9823,6 +9855,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10168,6 +10201,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10392,6 +10426,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10839,6 +10874,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"currentItemBackgroundColor": "#a5d8ff",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11098,6 +11134,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11332,6 +11369,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11568,6 +11606,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11695,6 +11734,11 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -11723,14 +11767,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -11749,6 +11789,11 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -11777,14 +11822,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#e03131",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -11893,6 +11934,11 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -11920,14 +11966,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#e03131",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -11970,6 +12012,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12179,6 +12222,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12388,6 +12432,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12611,6 +12656,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12834,6 +12880,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13078,6 +13125,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13314,6 +13362,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13550,6 +13599,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13796,6 +13846,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14126,6 +14177,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14295,6 +14347,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14578,6 +14631,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14840,6 +14894,7 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14992,6 +15047,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"currentItemBackgroundColor": "#a5d8ff",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -15273,6 +15329,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -15434,6 +15491,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -16132,6 +16190,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -16763,6 +16822,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -17394,6 +17454,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -18106,6 +18167,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -18850,6 +18912,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -19329,6 +19392,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -19839,6 +19903,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -20297,6 +20362,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,

View File

@ -20,6 +20,7 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -445,6 +446,7 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -860,6 +862,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1425,6 +1428,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -1631,6 +1635,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2014,6 +2019,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2258,6 +2264,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2437,6 +2444,7 @@ exports[`regression tests > can drag element that covers another element, while
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -2761,6 +2769,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3015,6 +3024,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3255,6 +3265,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3490,6 +3501,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -3747,6 +3759,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4060,6 +4073,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4495,6 +4509,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -4777,6 +4792,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5052,6 +5068,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5259,6 +5276,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5458,6 +5476,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -5850,6 +5869,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6146,6 +6166,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -6900,6 +6921,11 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -6927,14 +6953,10 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -6977,6 +6999,7 @@ exports[`regression tests > given a group of selected elements with an element t
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7310,6 +7333,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"currentItemBackgroundColor": "#ffc9c9",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7588,6 +7612,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -7822,6 +7847,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8061,6 +8087,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8240,6 +8267,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8419,6 +8447,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8598,6 +8627,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -8823,6 +8853,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9046,6 +9077,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9164,6 +9196,11 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -9191,14 +9228,10 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -9241,6 +9274,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9466,6 +9500,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9645,6 +9680,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -9868,6 +9904,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10047,6 +10084,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10165,6 +10203,11 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.35000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -10192,14 +10235,10 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
10,
],
],
"pressures": [
0,
0,
0,
],
"pressures": [],
"roughness": 1,
"roundness": null,
"simulatePressure": false,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -10242,6 +10281,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10421,6 +10461,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -10951,6 +10992,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11230,6 +11272,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11352,6 +11395,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11551,6 +11595,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -11869,6 +11914,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12297,6 +12343,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -12936,6 +12983,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13061,6 +13109,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -13691,6 +13740,7 @@ exports[`regression tests > switches from group of selected elements to another
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14029,6 +14079,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14292,6 +14343,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14414,6 +14466,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14802,6 +14855,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 8,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
@ -14924,6 +14978,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,

View File

@ -78,7 +78,7 @@ describe("actionStyles", () => {
expect(firstRect.strokeColor).toBe("#e03131");
expect(firstRect.backgroundColor).toBe("#a5d8ff");
expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeWidth).toBe(4); // Bold: 4
expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60);

View File

@ -381,7 +381,7 @@ describe("contextMenu element", () => {
expect(firstRect.strokeColor).toBe("#e03131");
expect(firstRect.backgroundColor).toBe("#a5d8ff");
expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeWidth).toBe(4); // Bold: 4
expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60);

View File

@ -168,6 +168,11 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"backgroundColor": "transparent",
"boundElements": [],
"customData": undefined,
"drawingConfigs": {
"fixedStrokeWidth": true,
"simplify": "0.10000",
"streamline": "0.25000",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],

View File

@ -331,6 +331,7 @@ export interface AppState {
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: number;
currentItemOpacity: number;
currentItemFixedStrokeWidth: boolean;
currentItemFontFamily: FontFamilyValues;
currentItemFontSize: number;
currentItemTextAlign: TextAlign;

View File

@ -1,6 +1,6 @@
{
"name": "@excalidraw/math",
"version": "0.1.0",
"version": "0.18.0",
"type": "module",
"types": "./dist/types/math/src/index.d.ts",
"main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js"
},
"./*": {
"types": "./dist/types/math/src/*.d.ts"
"types": "./dist/types/math/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
}
},
"files": [
@ -56,5 +59,8 @@
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
},
"dependencies": {
"@excalidraw/common": "0.18.0"
}
}

View File

@ -49,7 +49,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/laser-pointer": "1.3.2",
"browser-fs-access": "0.29.1",
"open-color": "1.9.1",
"pako": "2.0.3",

View File

@ -20,6 +20,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFixedStrokeWidth": true,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,

View File

@ -1,71 +0,0 @@
const { exec, execSync } = require("child_process");
const fs = require("fs");
const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const publish = () => {
const tag = isPreview ? "preview" : "next";
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
console.info(`Published ${pkg.name}@${tag}🎉`);
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
console.error(error);
process.exit(1);
}
};
// get files changed between prev and head commit
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
if (error || stderr) {
console.error(error);
core.setOutput("result", ":warning: Package couldn't be published!");
process.exit(1);
}
const changedFiles = stdout.trim().split("\n");
const excalidrawPackageFiles = changedFiles.filter((file) => {
return (
file.indexOf("packages/excalidraw") >= 0 ||
file.indexOf("buildPackage.js") > 0
);
});
if (!excalidrawPackageFiles.length) {
console.info("Skipping release as no valid diff found");
core.setOutput("result", "Skipping release as no valid diff found");
process.exit(0);
}
// update package.json
let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme
if (isPreview) {
// use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
}
pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
console.info("Publish in progress...");
publish();
});

View File

@ -11,12 +11,9 @@ const getConfig = (outdir) => ({
entryNames: "[name]",
assetNames: "[dir]/[name]",
alias: {
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
},
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
});
function buildDev(config) {

View File

@ -28,12 +28,9 @@ const getConfig = (outdir) => ({
assetNames: "[dir]/[name]",
chunkNames: "[dir]/[name]-[hash]",
alias: {
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
},
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
loader: {
".woff2": "file",
},

View File

@ -1,38 +0,0 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../packages/excalidraw/`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const prerelease = async (nextVersion) => {
try {
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
console.info("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
prerelease(nextVersion);

View File

@ -1,28 +1,239 @@
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const updateChangelog = require("./updateChangelog");
const publish = () => {
try {
console.info("Installing the dependencies in root folder...");
execSync(`yarn --frozen-lockfile`);
console.info("Installing the dependencies in excalidraw directory...");
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
console.info("Building ESM Package...");
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
console.info("Publishing the package...");
execSync(`yarn --cwd ${excalidrawDir} publish`);
} catch (error) {
console.error(error);
// skipping utils for now, as it has independent release process
const PACKAGES = ["common", "math", "element", "excalidraw"];
const PACKAGES_DIR = path.resolve(__dirname, "../packages");
/**
* Returns the arguments for the release script.
*
* Usage examples:
* - yarn release --help -> prints this help message
* - yarn release -> publishes `@excalidraw` packages with "test" tag and "-[hash]" version suffix
* - yarn release --tag=test -> same as above
* - yarn release --tag=next -> publishes `@excalidraw` packages with "next" tag and version "-[hash]" suffix
* - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
* - yarn release --tag=latest --version=0.19.0 -> publishes `@excalidraw` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
*
* @returns [tag, version, nonInteractive]
*/
const getArguments = () => {
let tag = "test";
let version = "";
let nonInteractive = false;
for (const argument of process.argv.slice(2)) {
if (/--help/.test(argument)) {
console.info(`Available arguments:
--tag=<tag> -> (optional) "test" (default), "next" for auto release, "latest" for stable release
--version=<version> -> (optional) for "next" and "test", (required) for "latest" i.e. "0.19.0"
--non-interactive -> (optional) disables interactive prompts`);
console.info(`\nUsage examples:
- yarn release -> publishes \`@excalidraw\` packages with "test" tag and "-[hash]" version suffix
- yarn release --tag=test -> same as above
- yarn release --tag=next -> publishes \`@excalidraw\` packages with "next" tag and version "-[hash]" suffix
- yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
- yarn release --tag=latest --version=0.19.0 -> publishes \`@excalidraw\` packages with "latest" tag and version "0.19.0" & prepares changelog for the release`);
process.exit(0);
}
if (/--tag=/.test(argument)) {
tag = argument.split("=")[1];
}
if (/--version=/.test(argument)) {
version = argument.split("=")[1];
}
if (/--non-interactive/.test(argument)) {
nonInteractive = true;
}
}
if (tag !== "latest" && tag !== "next" && tag !== "test") {
console.error(`Unsupported tag "${tag}", use "latest", "next" or "test".`);
process.exit(1);
}
if (tag === "latest" && !version) {
console.error("Pass the version to make the latest stable release!");
process.exit(1);
}
if (!version) {
// set the next version based on the excalidraw package version + commit hash
const excalidrawPackageVersion = require(getPackageJsonPath(
"excalidraw",
)).version;
const hash = getShortCommitHash();
if (!excalidrawPackageVersion.includes(hash)) {
version = `${excalidrawPackageVersion}-${hash}`;
} else {
// ensuring idempotency
version = excalidrawPackageVersion;
}
}
console.info(`Running with tag "${tag}" and version "${version}"...`);
return [tag, version, nonInteractive];
};
const validatePackageName = (packageName) => {
if (!PACKAGES.includes(packageName)) {
console.error(`Package "${packageName}" not found!`);
process.exit(1);
}
};
const release = () => {
publish();
console.info(`Published ${pkg.version}!`);
const getPackageJsonPath = (packageName) => {
validatePackageName(packageName);
return path.resolve(PACKAGES_DIR, packageName, "package.json");
};
release();
const updatePackageJsons = (nextVersion) => {
const packageJsons = new Map();
for (const packageName of PACKAGES) {
const pkg = require(getPackageJsonPath(packageName));
pkg.version = nextVersion;
if (pkg.dependencies) {
for (const dependencyName of PACKAGES) {
if (!pkg.dependencies[`@excalidraw/${dependencyName}`]) {
continue;
}
pkg.dependencies[`@excalidraw/${dependencyName}`] = nextVersion;
}
}
packageJsons.set(packageName, `${JSON.stringify(pkg, null, 2)}\n`);
}
// modify once, to avoid inconsistent state
for (const packageName of PACKAGES) {
const content = packageJsons.get(packageName);
fs.writeFileSync(getPackageJsonPath(packageName), content, "utf-8");
}
};
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const askToCommit = (tag, nextVersion) => {
if (tag !== "latest") {
return Promise.resolve();
}
return new Promise((resolve) => {
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(
"Do you want to commit these changes to git? (Y/n): ",
(answer) => {
rl.close();
if (answer.toLowerCase() === "y") {
execSync(`git add -u`);
execSync(
`git commit -m "chore: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
} else {
console.warn(
"Skipping commit. Don't forget to commit manually later!",
);
}
resolve();
},
);
});
};
const buildPackages = () => {
console.info("Running yarn install...");
execSync(`yarn --frozen-lockfile`, { stdio: "inherit" });
console.info("Removing existing build artifacts...");
execSync(`yarn rm:build`, { stdio: "inherit" });
for (const packageName of PACKAGES) {
console.info(`Building "@excalidraw/${packageName}"...`);
execSync(`yarn run build:esm`, {
cwd: path.resolve(PACKAGES_DIR, packageName),
stdio: "inherit",
});
}
};
const askToPublish = (tag, version) => {
return new Promise((resolve) => {
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(
"Do you want to publish these changes to npm? (Y/n): ",
(answer) => {
rl.close();
if (answer.toLowerCase() === "y") {
publishPackages(tag, version);
} else {
console.info("Skipping publish.");
}
resolve();
},
);
});
};
const publishPackages = (tag, version) => {
for (const packageName of PACKAGES) {
execSync(`yarn publish --tag ${tag}`, {
cwd: path.resolve(PACKAGES_DIR, packageName),
stdio: "inherit",
});
console.info(
`Published "@excalidraw/${packageName}@${tag}" with version "${version}"! 🎉`,
);
}
};
/** main */
(async () => {
const [tag, version, nonInteractive] = getArguments();
buildPackages();
if (tag === "latest") {
await updateChangelog(version);
}
updatePackageJsons(version);
if (nonInteractive) {
publishPackages(tag, version);
} else {
await askToCommit(tag, version);
await askToPublish(tag, version);
}
})();

View File

@ -20,14 +20,16 @@ const headerForType = {
perf: "Performance",
build: "Build",
};
const badCommits = [];
const getCommitHashForLastVersion = async () => {
try {
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
const commitMessage = `"release @excalidraw/excalidraw"`;
const { stdout } = await exec(
`git log --format=format:"%H" --grep=${commitMessage}`,
);
return stdout;
// take commit hash from latest release
return stdout.split(/\r?\n/)[0];
} catch (error) {
console.error(error);
}

View File

@ -1442,10 +1442,10 @@
resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd"
integrity sha512-GemHNF5Z6ga0BWBSX7GJaNBUchLu6RwTcAB84eX1MeckRNhNasAsPCdelDlFalz27iS4RuYEQh0bPE8SRxJgbQ==
"@excalidraw/laser-pointer@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz#7c40836598e8e6ad91f01057883ed8b88fb9266c"
integrity sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==
"@excalidraw/laser-pointer@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.3.2.tgz#1f91182dfc77291df72551c83fa7bf9d740ad9ae"
integrity sha512-aKhVj3/lLV7TCkZr6q5kn9sBFxzpOQ/yyRwBGm6smsakMdp0o1FVp9HNKOPerEpkKEt34VTURekZLfqa3E1Few==
"@excalidraw/markdown-to-text@0.1.2":
version "0.1.2"