Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c93e2fa9ce | |||
| 47ee8a0094 | |||
| 039562cd61 | |||
| 629cd307fd | |||
| 81e3dd5406 | |||
| 704bbd6e0f | |||
| bff220e0f5 | |||
| ce595ff18c | |||
| 1dfadb4d26 | |||
| 3fb902f1d8 | |||
| 63a91a883f | |||
| 5164bdb782 | |||
| 9642a6e756 | |||
| f71ded4bf9 | |||
| 3aa1365acb | |||
| 00691631d8 | |||
| cbb349e34b | |||
| 453757756d | |||
| c456c1e713 | |||
| daf305af34 | |||
| 6966a1022c | |||
| fc7ea757b2 | |||
| e5934f23c0 | |||
| 1cad91ca5f | |||
| 6b2e5516ca | |||
| dd4bf91128 | |||
| ef0fcc1537 | |||
| ec26aeead2 | |||
| 62f5475c4a | |||
| 7225915b82 | |||
| 8eb3191b3f | |||
| 4d6d6cf129 | |||
| 208285b7ba | |||
| 372a4868da | |||
| 05800d8599 | |||
| 1f496d9f64 | |||
| e0221ddf20 | |||
| 1bd86942f3 | |||
| fd9a172da9 | |||
| 1f9847ed98 | |||
| 4e4802b19e | |||
| 23eb08088e | |||
| e8a6053251 | |||
| 456433e8f0 | |||
| 38e3a4e8e1 | |||
| 27a8cda8fd | |||
| dd5053149a | |||
| 40ec02b280 | |||
| b81aa19ff9 | |||
| e4ddd08bb1 | |||
| 795176b256 | |||
| be057bde39 | |||
| 94f4b727bb | |||
| 63698572db | |||
| ab3467973f | |||
| 91fe07d9c5 | |||
| 28cc821047 | |||
| 7dc728a459 | |||
| 12c651af6d | |||
| 9d0cafe10b | |||
| fb24221587 | |||
| ef347cc685 | |||
| 2d3b9e0c66 | |||
| bdb0dd064b | |||
| b17ed4dc29 | |||
| b988f67759 | |||
| 089aaa8792 | |||
| 28261c4b29 | |||
| 3fbed86d3e | |||
| 38b3d90fa6 | |||
| 82b597ab8b | |||
| 4c939cefad | |||
| 8f0d9f5230 | |||
| fcde0ac3de | |||
| b07dfba4b8 | |||
| 1089cdb278 | |||
| 7246a6b17a | |||
| 04a96caf78 | |||
| 14c6ea938a | |||
| 87aba3f619 | |||
| c8d4e8c421 | |||
| 512e506798 | |||
| b4e742bda0 | |||
| 5a3f4fd08f | |||
| 34515f2952 | |||
| 08f430b3ac | |||
| 59e74f94e6 | |||
| ddc393bd9d | |||
| 9e5948ac28 | |||
| f86d0f9102 | |||
| ace031e992 | |||
| 45faf7d58f | |||
| 8c558a0f33 | |||
| 65059cb166 | |||
| 9158e2d989 | |||
| 12da1862a0 | |||
| 67fb3210ab | |||
| 13d69d8cef | |||
| 0f6ad916c0 | |||
| 9ee2bf36cf | |||
| 86f5c2ebcf |
@@ -4,6 +4,7 @@ import { trackEvent } from "../packages/excalidraw/analytics";
|
|||||||
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
||||||
import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
|
import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||||
|
import { useMathSubtype } from "../packages/excalidraw/element/subtypes/mathjax";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
@@ -355,6 +356,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
const [excalidrawAPI, excalidrawRefCallback] =
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||||
|
|
||||||
|
useMathSubtype(excalidrawAPI);
|
||||||
|
|
||||||
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jsdom": "22.1.0",
|
"jsdom": "22.1.0",
|
||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
|
"patch-package": "8.0.0",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
|
"postinstall-postinstall": "2.1.0",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
@@ -61,6 +63,7 @@
|
|||||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
|
"postinstall": "patch-package",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "yarn --cwd ./excalidraw-app start",
|
"start": "yarn --cwd ./excalidraw-app start",
|
||||||
"start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ define: {
|
|||||||
|
|
||||||
## 0.16.0 (2023-09-19)
|
## 0.16.0 (2023-09-19)
|
||||||
|
|
||||||
|
- Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037).
|
||||||
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||||
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||||
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,7 @@ import type {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { arrayToMap, getFontString } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { syncMovedIndices } from "../fractionalIndex";
|
import { syncMovedIndices } from "../fractionalIndex";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
@@ -51,11 +51,9 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height } = measureText(
|
const { width, height } = measureTextElement(boundTextElement, {
|
||||||
boundTextElement.originalText,
|
text: boundTextElement.originalText,
|
||||||
getFontString(boundTextElement),
|
});
|
||||||
boundTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
|||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { resetCursor } from "../cursor";
|
import { resetCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
import { isPathALoop } from "../shapes";
|
import { isPathALoop } from "../shapes";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
@@ -115,7 +115,7 @@ export const actionFinalize = register({
|
|||||||
mutateElement(multiPointElement, {
|
mutateElement(multiPointElement, {
|
||||||
points: linePoints.map((p, index) =>
|
points: linePoints.map((p, index) =>
|
||||||
index === linePoints.length - 1
|
index === linePoints.length - 1
|
||||||
? point(firstPoint[0], firstPoint[1])
|
? pointFrom(firstPoint[0], firstPoint[1])
|
||||||
: p,
|
: p,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { render } from "../tests/test-utils";
|
import { render } from "../tests/test-utils";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@@ -50,11 +50,11 @@ describe("flipping re-centers selection", () => {
|
|||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(0, -35),
|
pointFrom(0, -35),
|
||||||
point(-90.9, -35),
|
pointFrom(-90.9, -35),
|
||||||
point(-90.9, 204.9),
|
pointFrom(-90.9, 204.9),
|
||||||
point(65.1, 204.9),
|
pointFrom(65.1, 204.9),
|
||||||
],
|
],
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ import {
|
|||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { point, vector } from "../../math";
|
import { pointFrom, vector } from "../../math";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
@@ -1651,7 +1651,7 @@ export const actionChangeArrowType = register({
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
[finalStartPoint, finalEndPoint].map(
|
[finalStartPoint, finalEndPoint].map(
|
||||||
(p): LocalPoint =>
|
(p): LocalPoint =>
|
||||||
point(p[0] - newElement.x, p[1] - newElement.y),
|
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||||
),
|
),
|
||||||
vector(0, 0),
|
vector(0, 0),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
|
ActionPredicateFn,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -45,6 +46,7 @@ const trackAction = (
|
|||||||
|
|
||||||
export class ActionManager {
|
export class ActionManager {
|
||||||
actions = {} as Record<ActionName, Action>;
|
actions = {} as Record<ActionName, Action>;
|
||||||
|
actionPredicates = [] as ActionPredicateFn[];
|
||||||
|
|
||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
@@ -72,6 +74,37 @@ export class ActionManager {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerActionPredicate(predicate: ActionPredicateFn) {
|
||||||
|
if (!this.actionPredicates.includes(predicate)) {
|
||||||
|
this.actionPredicates.push(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterActions(
|
||||||
|
filter: ActionPredicateFn,
|
||||||
|
opts?: {
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
data?: Record<string, any>;
|
||||||
|
},
|
||||||
|
): Action[] {
|
||||||
|
// For testing
|
||||||
|
if (this === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||||
|
const appState = this.getAppState();
|
||||||
|
const data = opts?.data;
|
||||||
|
|
||||||
|
const actions: Action[] = [];
|
||||||
|
for (const key in this.actions) {
|
||||||
|
const action = this.actions[key as ActionName];
|
||||||
|
if (filter(action, elements, appState, this.app, data)) {
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
registerAction(action: Action) {
|
registerAction(action: Action) {
|
||||||
this.actions[action.name] = action;
|
this.actions[action.name] = action;
|
||||||
}
|
}
|
||||||
@@ -88,7 +121,7 @@ export class ActionManager {
|
|||||||
(action) =>
|
(action) =>
|
||||||
(action.name in canvasActions
|
(action.name in canvasActions
|
||||||
? canvasActions[action.name as keyof typeof canvasActions]
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
: true) &&
|
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -147,7 +180,7 @@ export class ActionManager {
|
|||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
(name in canvasActions
|
(name in canvasActions
|
||||||
? canvasActions[name as keyof typeof canvasActions]
|
? canvasActions[name as keyof typeof canvasActions]
|
||||||
: true)
|
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@@ -169,6 +202,7 @@ export class ActionManager {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
|
key={name}
|
||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
@@ -182,13 +216,31 @@ export class ActionManager {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (action: Action) => {
|
isActionEnabled = (
|
||||||
const elements = this.getElementsIncludingDeleted();
|
action: Action,
|
||||||
|
opts?: {
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
data?: Record<string, any>;
|
||||||
|
noPredicates?: boolean;
|
||||||
|
},
|
||||||
|
): boolean => {
|
||||||
|
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
|
const data = opts?.data;
|
||||||
|
|
||||||
return (
|
if (
|
||||||
!action.predicate ||
|
!opts?.noPredicates &&
|
||||||
action.predicate(elements, appState, this.app.props, this.app)
|
action.predicate &&
|
||||||
);
|
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let enabled = true;
|
||||||
|
this.actionPredicates.forEach((fn) => {
|
||||||
|
if (!fn(action, elements, appState, this.app, data)) {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { isDarwin } from "../constants";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import type { SubtypeOf } from "../utility-types";
|
import type { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import type { ActionName } from "./types";
|
import type { ActionName, CustomActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName =
|
||||||
| SubtypeOf<
|
| SubtypeOf<
|
||||||
ActionName,
|
ActionName,
|
||||||
|
| CustomActionName
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "clearCanvas"
|
| "clearCanvas"
|
||||||
@@ -54,6 +55,15 @@ export type ShortcutName =
|
|||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "searchMenu";
|
| "searchMenu";
|
||||||
|
|
||||||
|
export const registerCustomShortcuts = (
|
||||||
|
shortcuts: Record<CustomActionName, string[]>,
|
||||||
|
) => {
|
||||||
|
for (const key in shortcuts) {
|
||||||
|
const shortcut = key as CustomActionName;
|
||||||
|
shortcutMap[shortcut] = shortcuts[shortcut];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
|
|||||||
@@ -41,10 +41,24 @@ type ActionFn = (
|
|||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
|
// Return `true` *unless* `Action` should be disabled
|
||||||
|
// given `elements`, `appState`, and optionally `data`.
|
||||||
|
export type ActionPredicateFn = (
|
||||||
|
action: Action,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
) => boolean;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
export type ActionFilterFn = (action: Action) => void;
|
export type ActionFilterFn = (action: Action) => void;
|
||||||
|
|
||||||
|
export const makeCustomActionName = (name: string) =>
|
||||||
|
`custom.${name}` as CustomActionName;
|
||||||
|
export type CustomActionName = `custom.${string}`;
|
||||||
export type ActionName =
|
export type ActionName =
|
||||||
|
| CustomActionName
|
||||||
| "copy"
|
| "copy"
|
||||||
| "cut"
|
| "cut"
|
||||||
| "paste"
|
| "paste"
|
||||||
@@ -179,6 +193,7 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
appProps: ExcalidrawProps,
|
appProps: ExcalidrawProps,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
|
data?: Record<string, any>,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
trackEvent:
|
trackEvent:
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false, server: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
activeTool: { browser: true, export: false, server: false },
|
activeTool: { browser: true, export: false, server: false },
|
||||||
|
activeSubtypes: { browser: true, export: false, server: false },
|
||||||
|
customData: { browser: true, export: false, server: false },
|
||||||
penMode: { browser: true, export: false, server: false },
|
penMode: { browser: true, export: false, server: false },
|
||||||
penDetected: { browser: true, export: false, server: false },
|
penDetected: { browser: true, export: false, server: false },
|
||||||
errorMessage: { browser: false, export: false, server: false },
|
errorMessage: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Radians } from "../math";
|
import type { Radians } from "../math";
|
||||||
import { point } from "../math";
|
import { pointFrom } from "../math";
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
DEFAULT_CHART_COLOR_INDEX,
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import type { NonDeletedExcalidrawElement } from "./element/types";
|
import type { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
|
import type { AppState } from "./types";
|
||||||
|
import { selectSubtype } from "./element/subtypes";
|
||||||
|
|
||||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ export interface Spreadsheet {
|
|||||||
title: string | null;
|
title: string | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
values: number[];
|
values: number[];
|
||||||
|
activeSubtypes?: AppState["activeSubtypes"];
|
||||||
|
customData?: AppState["customData"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||||
@@ -195,13 +199,17 @@ const chartXLabels = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
|
const custom = selectSubtype(spreadsheet, "text");
|
||||||
return (
|
return (
|
||||||
spreadsheet.labels?.map((label, index) => {
|
spreadsheet.labels?.map((label, index) => {
|
||||||
return newTextElement({
|
return newTextElement({
|
||||||
groupIds: [groupId],
|
groupIds: [groupId],
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
...commonProps,
|
...commonProps,
|
||||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
text:
|
||||||
|
label.length > 8 && custom.subtype === undefined
|
||||||
|
? `${label.slice(0, 5)}...`
|
||||||
|
: label,
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
@@ -209,6 +217,7 @@ const chartXLabels = (
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
|
...custom,
|
||||||
});
|
});
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
@@ -229,6 +238,7 @@ const chartYLabels = (
|
|||||||
y: y - BAR_GAP,
|
y: y - BAR_GAP,
|
||||||
text: "0",
|
text: "0",
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxYLabel = newTextElement({
|
const maxYLabel = newTextElement({
|
||||||
@@ -239,6 +249,7 @@ const chartYLabels = (
|
|||||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return [minYLabel, maxYLabel];
|
return [minYLabel, maxYLabel];
|
||||||
@@ -260,7 +271,8 @@ const chartLines = (
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
points: [point(0, 0), point(chartWidth, 0)],
|
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
const yLine = newLinearElement({
|
||||||
@@ -271,7 +283,8 @@ const chartLines = (
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
points: [point(0, 0), point(0, -chartHeight)],
|
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
const maxLine = newLinearElement({
|
||||||
@@ -284,7 +297,8 @@ const chartLines = (
|
|||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [point(0, 0), point(chartWidth, 0)],
|
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
return [xLine, yLine, maxLine];
|
||||||
@@ -311,6 +325,7 @@ const chartBaseElements = (
|
|||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -327,6 +342,7 @@ const chartBaseElements = (
|
|||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
|
...selectSubtype(spreadsheet, "rectangle"),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -359,6 +375,7 @@ const chartTypeBar = (
|
|||||||
y: y - barHeight - BAR_GAP,
|
y: y - barHeight - BAR_GAP,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
|
...selectSubtype(spreadsheet, "rectangle"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -409,6 +426,7 @@ const chartTypeLine = (
|
|||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: points as any,
|
points: points as any,
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dots = spreadsheet.values.map((value, index) => {
|
const dots = spreadsheet.values.map((value, index) => {
|
||||||
@@ -425,6 +443,7 @@ const chartTypeLine = (
|
|||||||
y: y + cy - BAR_GAP * 2,
|
y: y + cy - BAR_GAP * 2,
|
||||||
width: BAR_GAP,
|
width: BAR_GAP,
|
||||||
height: BAR_GAP,
|
height: BAR_GAP,
|
||||||
|
...selectSubtype(spreadsheet, "ellipse"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -441,7 +460,8 @@ const chartTypeLine = (
|
|||||||
height: cy,
|
height: cy,
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [point(0, 0), point(0, cy)],
|
points: [pointFrom(0, 0), pointFrom(0, cy)],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import type { BinaryFiles } from "./types";
|
import type { AppState, BinaryFiles } from "./types";
|
||||||
import type { Spreadsheet } from "./charts";
|
import type { Spreadsheet } from "./charts";
|
||||||
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import {
|
import {
|
||||||
@@ -333,6 +333,7 @@ const parseClipboardEvent = async (
|
|||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
|
appState?: AppState,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
||||||
|
|
||||||
@@ -349,6 +350,10 @@ export const parseClipboard = async (
|
|||||||
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
if (spreadsheetResult) {
|
||||||
|
if ("spreadsheet" in spreadsheetResult) {
|
||||||
|
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
|
||||||
|
spreadsheetResult.spreadsheet.customData = appState?.customData;
|
||||||
|
}
|
||||||
return spreadsheetResult;
|
return spreadsheetResult;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
|||||||
import { capitalizeString, isTransparent } from "../utils";
|
import { capitalizeString, isTransparent } from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { SubtypeShapeActions } from "./Subtypes";
|
||||||
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
|
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import {
|
import {
|
||||||
@@ -136,6 +137,7 @@ export const SelectedShapeActions = ({
|
|||||||
{canChangeBackgroundColor(appState, targetElements) && (
|
{canChangeBackgroundColor(appState, targetElements) && (
|
||||||
<div>{renderAction("changeBackgroundColor")}</div>
|
<div>{renderAction("changeBackgroundColor")}</div>
|
||||||
)}
|
)}
|
||||||
|
<SubtypeShapeActions elements={targetElements} />
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
|
|||||||
@@ -301,6 +301,18 @@ import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
|||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
import type {
|
||||||
|
SubtypeLoadedCb,
|
||||||
|
SubtypeRecord,
|
||||||
|
SubtypePrepFn,
|
||||||
|
} from "../element/subtypes";
|
||||||
|
import {
|
||||||
|
checkRefreshOnSubtypeLoad,
|
||||||
|
isSubtypeAction,
|
||||||
|
prepareSubtype,
|
||||||
|
selectSubtype,
|
||||||
|
subtypeActionPredicate,
|
||||||
|
} from "../element/subtypes";
|
||||||
import {
|
import {
|
||||||
dataURLToFile,
|
dataURLToFile,
|
||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
@@ -445,7 +457,7 @@ import {
|
|||||||
} from "../element/flowchart";
|
} from "../element/flowchart";
|
||||||
import { searchItemInFocusAtom } from "./SearchMenu";
|
import { searchItemInFocusAtom } from "./SearchMenu";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { point, pointDistance, vector } from "../../math";
|
import { pointFrom, pointDistance, vector } from "../../math";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -710,6 +722,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
registerAction: (action: Action) => {
|
registerAction: (action: Action) => {
|
||||||
this.actionManager.registerAction(action);
|
this.actionManager.registerAction(action);
|
||||||
},
|
},
|
||||||
|
addSubtype: this.addSubtype,
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
setToast: this.setToast,
|
setToast: this.setToast,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@@ -746,6 +759,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.actionManager.registerAction(
|
this.actionManager.registerAction(
|
||||||
createRedoAction(this.history, this.store),
|
createRedoAction(this.history, this.store),
|
||||||
);
|
);
|
||||||
|
this.actionManager.registerActionPredicate(subtypeActionPredicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
||||||
|
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||||
|
const elements = this.getSceneElementsIncludingDeleted();
|
||||||
|
// If there are any elements of the just-registered subtype,
|
||||||
|
// refresh the scene to re-render each such element.
|
||||||
|
if (checkRefreshOnSubtypeLoad(hasSubtype, elements)) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWindowMessage(event: MessageEvent) {
|
private onWindowMessage(event: MessageEvent) {
|
||||||
@@ -2951,7 +2977,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// event else some browsers (FF...) will clear the clipboardData
|
// event else some browsers (FF...) will clear the clipboardData
|
||||||
// (something something security)
|
// (something something security)
|
||||||
let file = event?.clipboardData?.files[0];
|
let file = event?.clipboardData?.files[0];
|
||||||
const data = await parseClipboard(event, isPlainPaste);
|
const data = await parseClipboard(event, isPlainPaste, this.state);
|
||||||
if (!file && !isPlainPaste) {
|
if (!file && !isPlainPaste) {
|
||||||
if (data.mixedContent) {
|
if (data.mixedContent) {
|
||||||
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
||||||
@@ -3389,6 +3415,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
textAlign: DEFAULT_TEXT_ALIGN,
|
textAlign: DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||||
|
...selectSubtype(this.state, "text"),
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
const fontString = getFontString({
|
const fontString = getFontString({
|
||||||
@@ -4910,7 +4937,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.getElementHitThreshold(),
|
this.getElementHitThreshold(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isPointInShape(point(x, y), selectionShape);
|
return isPointInShape(pointFrom(x, y), selectionShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
// take bound text element into consideration for hit collision as well
|
// take bound text element into consideration for hit collision as well
|
||||||
@@ -5098,6 +5125,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
verticalAlign: parentCenterPosition
|
verticalAlign: parentCenterPosition
|
||||||
? VERTICAL_ALIGN.MIDDLE
|
? VERTICAL_ALIGN.MIDDLE
|
||||||
: DEFAULT_VERTICAL_ALIGN,
|
: DEFAULT_VERTICAL_ALIGN,
|
||||||
|
...selectSubtype(this.state, "text"),
|
||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
lineHeight,
|
lineHeight,
|
||||||
@@ -5269,7 +5297,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
point(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -5281,11 +5309,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isTouchScreen: boolean,
|
isTouchScreen: boolean,
|
||||||
) => {
|
) => {
|
||||||
const draggedDistance = pointDistance(
|
const draggedDistance = pointDistance(
|
||||||
point(
|
pointFrom(
|
||||||
this.lastPointerDownEvent!.clientX,
|
this.lastPointerDownEvent!.clientX,
|
||||||
this.lastPointerDownEvent!.clientY,
|
this.lastPointerDownEvent!.clientY,
|
||||||
),
|
),
|
||||||
point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY),
|
pointFrom(
|
||||||
|
this.lastPointerUpEvent!.clientX,
|
||||||
|
this.lastPointerUpEvent!.clientY,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!this.hitLinkElement ||
|
!this.hitLinkElement ||
|
||||||
@@ -5304,7 +5335,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
point(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
@@ -5315,7 +5346,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
point(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
@@ -5565,7 +5596,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// threshold, add a point
|
// threshold, add a point
|
||||||
if (
|
if (
|
||||||
pointDistance(
|
pointDistance(
|
||||||
point(scenePointerX - rx, scenePointerY - ry),
|
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||||
lastPoint,
|
lastPoint,
|
||||||
) >= LINE_CONFIRM_THRESHOLD
|
) >= LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -5574,7 +5605,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
...points,
|
...points,
|
||||||
point<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -5588,7 +5619,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
points.length > 2 &&
|
points.length > 2 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
pointDistance(
|
pointDistance(
|
||||||
point(scenePointerX - rx, scenePointerY - ry),
|
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -5636,7 +5667,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
[
|
[
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
point<LocalPoint>(
|
pointFrom<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
),
|
),
|
||||||
@@ -5655,7 +5686,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
point<LocalPoint>(
|
pointFrom<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
),
|
),
|
||||||
@@ -5884,8 +5915,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||||
point(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
const threshold = this.getElementHitThreshold();
|
const threshold = this.getElementHitThreshold();
|
||||||
const p = { ...pointerDownState.lastCoords };
|
const p = { ...pointerDownState.lastCoords };
|
||||||
@@ -6397,7 +6428,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
point(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||||
@@ -7088,7 +7119,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
simulatePressure,
|
simulatePressure,
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
points: [point<LocalPoint>(0, 0)],
|
points: [pointFrom<LocalPoint>(0, 0)],
|
||||||
pressures: simulatePressure ? [] : [event.pressure],
|
pressures: simulatePressure ? [] : [event.pressure],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7251,6 +7282,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
|
...selectSubtype(this.state, "image"),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
@@ -7297,7 +7329,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
multiElement.points.length > 1 &&
|
multiElement.points.length > 1 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
pointDistance(
|
pointDistance(
|
||||||
point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry),
|
pointFrom(
|
||||||
|
pointerDownState.origin.x - rx,
|
||||||
|
pointerDownState.origin.y - ry,
|
||||||
|
),
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -7364,6 +7399,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
null,
|
null,
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
|
...selectSubtype(this.state, elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||||
@@ -7383,6 +7419,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.currentItemRoundness === "round"
|
this.state.currentItemRoundness === "round"
|
||||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||||
: null,
|
: null,
|
||||||
|
...selectSubtype(this.state, elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
@@ -7399,7 +7436,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [...element.points, point<LocalPoint>(0, 0)],
|
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||||
});
|
});
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
@@ -7463,6 +7500,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
roundness: this.getCurrentItemRoundness(elementType),
|
roundness: this.getCurrentItemRoundness(elementType),
|
||||||
|
...selectSubtype(this.state, elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -7652,8 +7690,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
pointDistance(
|
pointDistance(
|
||||||
point(pointerCoords.x, pointerCoords.y),
|
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||||
point(pointerDownState.origin.x, pointerDownState.origin.y),
|
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||||
) < DRAGGING_THRESHOLD
|
) < DRAGGING_THRESHOLD
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -8002,7 +8040,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, point<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -8031,7 +8069,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, point<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -8039,7 +8077,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElbowArrow(
|
mutateElbowArrow(
|
||||||
newElement,
|
newElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
[...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||||
vector(0, 0),
|
vector(0, 0),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -8051,7 +8089,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -8360,9 +8398,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: [...newElement.pressures, childEvent.pressure];
|
: [...newElement.pressures, childEvent.pressure];
|
||||||
|
|
||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [...points, point<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
lastCommittedPoint: point<LocalPoint>(dx, dy),
|
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -8409,7 +8447,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [
|
points: [
|
||||||
...newElement.points,
|
...newElement.points,
|
||||||
point<LocalPoint>(
|
pointFrom<LocalPoint>(
|
||||||
pointerCoords.x - newElement.x,
|
pointerCoords.x - newElement.x,
|
||||||
pointerCoords.y - newElement.y,
|
pointerCoords.y - newElement.y,
|
||||||
),
|
),
|
||||||
@@ -8723,8 +8761,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.eraserTrail.endPath();
|
this.eraserTrail.endPath();
|
||||||
|
|
||||||
const draggedDistance = pointDistance(
|
const draggedDistance = pointDistance(
|
||||||
point(pointerStart.clientX, pointerStart.clientY),
|
pointFrom(pointerStart.clientX, pointerStart.clientY),
|
||||||
point(pointerEnd.clientX, pointerEnd.clientY),
|
pointFrom(pointerEnd.clientX, pointerEnd.clientY),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (draggedDistance === 0) {
|
if (draggedDistance === 0) {
|
||||||
@@ -10042,6 +10080,39 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const elementsToHighlight = new Set<ExcalidrawElement>();
|
const elementsToHighlight = new Set<ExcalidrawElement>();
|
||||||
selectedFrames.forEach((frame) => {
|
selectedFrames.forEach((frame) => {
|
||||||
|
const elementsInFrame = getFrameChildren(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
frame.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// keep elements' positions relative to their frames on frames resizing
|
||||||
|
if (transformHandleType) {
|
||||||
|
if (transformHandleType.includes("w")) {
|
||||||
|
elementsInFrame.forEach((element) => {
|
||||||
|
mutateElement(element, {
|
||||||
|
x:
|
||||||
|
frame.x +
|
||||||
|
(frameElementsOffsetsMap.get(frame.id + element.id)?.x || 0),
|
||||||
|
y:
|
||||||
|
frame.y +
|
||||||
|
(frameElementsOffsetsMap.get(frame.id + element.id)?.y || 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (transformHandleType.includes("n")) {
|
||||||
|
elementsInFrame.forEach((element) => {
|
||||||
|
mutateElement(element, {
|
||||||
|
x:
|
||||||
|
frame.x +
|
||||||
|
(frameElementsOffsetsMap.get(frame.id + element.id)?.x || 0),
|
||||||
|
y:
|
||||||
|
frame.y +
|
||||||
|
(frameElementsOffsetsMap.get(frame.id + element.id)?.y || 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getElementsInResizingFrame(
|
getElementsInResizingFrame(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
frame,
|
frame,
|
||||||
@@ -10062,6 +10133,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private getContextMenuItems = (
|
private getContextMenuItems = (
|
||||||
type: "canvas" | "element",
|
type: "canvas" | "element",
|
||||||
|
): ContextMenuItems => {
|
||||||
|
const subtype: ContextMenuItems = [];
|
||||||
|
this.actionManager
|
||||||
|
.filterActions(isSubtypeAction)
|
||||||
|
.forEach(
|
||||||
|
(action) =>
|
||||||
|
this.actionManager.isActionEnabled(action, { data: {} }) &&
|
||||||
|
subtype.push(action),
|
||||||
|
);
|
||||||
|
if (subtype.length > 0) {
|
||||||
|
subtype.push(CONTEXT_MENU_SEPARATOR);
|
||||||
|
}
|
||||||
|
const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
|
||||||
|
(item) =>
|
||||||
|
!item ||
|
||||||
|
item === CONTEXT_MENU_SEPARATOR ||
|
||||||
|
this.actionManager.isActionEnabled(item, { noPredicates: true }),
|
||||||
|
);
|
||||||
|
return [...subtype, ...standard];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getContextMenuItems = (
|
||||||
|
type: "canvas" | "element",
|
||||||
): ContextMenuItems => {
|
): ContextMenuItems => {
|
||||||
const options: ContextMenuItems = [];
|
const options: ContextMenuItems = [];
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
|||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
|
import { SubtypeToggles } from "./Subtypes";
|
||||||
import { LaserPointerButton } from "./LaserPointerButton";
|
import { LaserPointerButton } from "./LaserPointerButton";
|
||||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
@@ -299,6 +300,7 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
<SubtypeToggles />
|
||||||
{isCollaborating && (
|
{isCollaborating && (
|
||||||
<Island
|
<Island
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { PenModeButton } from "./PenModeButton";
|
|||||||
import { HandButton } from "./HandButton";
|
import { HandButton } from "./HandButton";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import { SubtypeToggles } from "./Subtypes";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
@@ -89,6 +90,7 @@ export const MobileMenu = ({
|
|||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
<SubtypeToggles />
|
||||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||||
<div className="mobile-misc-tools-container">
|
<div className="mobile-misc-tools-container">
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useLayoutEffect, useRef, useState } from "react";
|
|||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import type { ChartElements, Spreadsheet } from "../charts";
|
import type { ChartElements, Spreadsheet } from "../charts";
|
||||||
import { renderSpreadsheet } from "../charts";
|
import { renderSpreadsheet } from "../charts";
|
||||||
import type { ChartType } from "../element/types";
|
import type { ChartType, ElementsMap } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import type { UIAppState } from "../types";
|
import type { UIAppState } from "../types";
|
||||||
@@ -11,6 +11,12 @@ import { useApp } from "./App";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
|
||||||
import "./PasteChartDialog.scss";
|
import "./PasteChartDialog.scss";
|
||||||
|
import { ensureSubtypesLoaded } from "../element/subtypes";
|
||||||
|
import { isTextElement } from "../element";
|
||||||
|
import {
|
||||||
|
getContainerElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
} from "../element/textElement";
|
||||||
|
|
||||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
@@ -26,41 +32,64 @@ const ChartPreviewBtn = (props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!props.spreadsheet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = renderSpreadsheet(
|
|
||||||
props.chartType,
|
|
||||||
props.spreadsheet,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
setChartElements(elements);
|
|
||||||
let svg: SVGSVGElement;
|
let svg: SVGSVGElement;
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
svg = await exportToSvg(
|
(async () => {
|
||||||
elements,
|
let elements: ChartElements;
|
||||||
{
|
await ensureSubtypesLoaded(
|
||||||
exportBackground: false,
|
props.spreadsheet?.activeSubtypes ?? [],
|
||||||
viewBackgroundColor: oc.white,
|
() => {
|
||||||
},
|
if (!props.spreadsheet) {
|
||||||
null, // files
|
return;
|
||||||
);
|
}
|
||||||
svg.querySelector(".style-fonts")?.remove();
|
|
||||||
previewNode.replaceChildren();
|
|
||||||
previewNode.appendChild(svg);
|
|
||||||
|
|
||||||
if (props.selected) {
|
elements = renderSpreadsheet(
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
props.chartType,
|
||||||
}
|
props.spreadsheet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const elementsMap = new Map() as ElementsMap;
|
||||||
|
for (const element of elements) {
|
||||||
|
if (!element.isDeleted) {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elements.forEach(
|
||||||
|
(el) =>
|
||||||
|
isTextElement(el) &&
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
el,
|
||||||
|
getContainerElement(el, elementsMap),
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setChartElements(elements);
|
||||||
|
},
|
||||||
|
).then(async () => {
|
||||||
|
svg = await exportToSvg(
|
||||||
|
elements,
|
||||||
|
{
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
},
|
||||||
|
null, // files
|
||||||
|
);
|
||||||
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
|
if (props.selected) {
|
||||||
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
previewNode.replaceChildren();
|
|
||||||
};
|
|
||||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
|||||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
import { point, type GlobalPoint } from "../../../math";
|
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||||
|
|
||||||
interface MultiDimensionProps {
|
interface MultiDimensionProps {
|
||||||
property: "width" | "height";
|
property: "width" | "height";
|
||||||
@@ -182,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextHeight,
|
nextHeight,
|
||||||
initialHeight,
|
initialHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
@@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextHeight,
|
nextHeight,
|
||||||
initialHeight,
|
initialHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useMemo } from "react";
|
|||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { pointFrom, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
interface MultiPositionProps {
|
interface MultiPositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
@@ -44,8 +44,8 @@ const moveElements = (
|
|||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(origElement.x, origElement.y),
|
pointFrom(origElement.x, origElement.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ const moveGroupTo = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(latestElement.x, latestElement.y),
|
pointFrom(latestElement.x, latestElement.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
latestElement.angle,
|
latestElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -171,8 +171,8 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(origElement.x, origElement.y),
|
pointFrom(origElement.x, origElement.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -241,8 +241,8 @@ const MultiPosition = ({
|
|||||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(el.x, el.y),
|
pointFrom(el.x, el.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
el.angle,
|
el.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { DragInputCallbackType } from "./DragInput";
|
|||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { pointFrom, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
@@ -33,8 +33,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(origElement.x, origElement.y),
|
pointFrom(origElement.x, origElement.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ const Position = ({
|
|||||||
appState,
|
appState,
|
||||||
}: PositionProps) => {
|
}: PositionProps) => {
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
point(element.x + element.width / 2, element.y + element.height / 2),
|
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const value =
|
const value =
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { API } from "../../tests/helpers/api";
|
|||||||
import { actionGroup } from "../../actions";
|
import { actionGroup } from "../../actions";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
import type { Degrees } from "../../../math";
|
import type { Degrees } from "../../../math";
|
||||||
import { degreesToRadians, point, pointRotateRads } from "../../../math";
|
import { degreesToRadians, pointFrom, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -264,8 +264,8 @@ describe("stats for a generic element", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -283,8 +283,8 @@ describe("stats for a generic element", () => {
|
|||||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
testInputProperty(rectangle, "angle", "A", 0, 45);
|
||||||
|
|
||||||
let [newTopLeftX, newTopLeftY] = pointRotateRads(
|
let [newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -294,8 +294,8 @@ describe("stats for a generic element", () => {
|
|||||||
testInputProperty(rectangle, "angle", "A", 45, 66);
|
testInputProperty(rectangle, "angle", "A", 45, 66);
|
||||||
|
|
||||||
[newTopLeftX, newTopLeftY] = pointRotateRads(
|
[newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
||||||
@@ -311,8 +311,8 @@ describe("stats for a generic element", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
||||||
@@ -321,8 +321,8 @@ describe("stats for a generic element", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
||||||
@@ -334,8 +334,8 @@ describe("stats for a generic element", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
[currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
[currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||||
point(rectangle.x, rectangle.y),
|
pointFrom(rectangle.x, rectangle.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Radians } from "../../../math";
|
import type { Radians } from "../../../math";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { pointFrom, pointRotateRads } from "../../../math";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
@@ -231,8 +231,8 @@ export const moveElement = (
|
|||||||
originalElement.y + originalElement.height / 2,
|
originalElement.y + originalElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(originalElement.x, originalElement.y),
|
pointFrom(originalElement.x, originalElement.y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
originalElement.angle,
|
originalElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -240,8 +240,8 @@ export const moveElement = (
|
|||||||
const changeInY = newTopLeftY - topLeftY;
|
const changeInY = newTopLeftY - topLeftY;
|
||||||
|
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
point(newTopLeftX, newTopLeftY),
|
pointFrom(newTopLeftX, newTopLeftY),
|
||||||
point(cx + changeInX, cy + changeInY),
|
pointFrom(cx + changeInX, cy + changeInY),
|
||||||
-originalElement.angle as Radians,
|
-originalElement.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import type { Action } from "../actions/types";
|
||||||
|
import { makeCustomActionName } from "../actions/types";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import type { Subtype, SubtypeRecord } from "../element/subtypes";
|
||||||
|
import {
|
||||||
|
getSubtypeNames,
|
||||||
|
hasAlwaysEnabledActions,
|
||||||
|
isSubtypeAction,
|
||||||
|
isValidSubtype,
|
||||||
|
subtypeCollides,
|
||||||
|
} from "../element/subtypes";
|
||||||
|
import type { ExcalidrawElement, Theme } from "../element/types";
|
||||||
|
import {
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawContainer,
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
} from "./App";
|
||||||
|
import type { ContextMenuItems } from "./ContextMenu";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
|
||||||
|
export const SubtypeButton = (
|
||||||
|
subtype: Subtype,
|
||||||
|
parentType: SubtypeRecord["parents"][number],
|
||||||
|
icon: ({ theme }: { theme: Theme }) => JSX.Element,
|
||||||
|
key?: string,
|
||||||
|
) => {
|
||||||
|
const title = key !== undefined ? ` - ${getShortcutKey(key)}` : "";
|
||||||
|
const keyTest: Action["keyTest"] =
|
||||||
|
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
|
||||||
|
const subtypeAction: Action = {
|
||||||
|
name: makeCustomActionName(subtype),
|
||||||
|
label: t(`toolBar.${subtype}`),
|
||||||
|
trackEvent: false,
|
||||||
|
predicate: (...rest) => rest[4]?.subtype === subtype,
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const inactive = !appState.activeSubtypes?.includes(subtype) ?? true;
|
||||||
|
const activeSubtypes: Subtype[] = [];
|
||||||
|
if (appState.activeSubtypes) {
|
||||||
|
activeSubtypes.push(...appState.activeSubtypes);
|
||||||
|
}
|
||||||
|
let activated = false;
|
||||||
|
if (inactive) {
|
||||||
|
// Ensure `element.subtype` is well-defined
|
||||||
|
if (!subtypeCollides(subtype, activeSubtypes)) {
|
||||||
|
activeSubtypes.push(subtype);
|
||||||
|
activated = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Can only be active if appState.activeSubtypes is defined
|
||||||
|
// and contains subtype.
|
||||||
|
activeSubtypes.splice(activeSubtypes.indexOf(subtype), 1);
|
||||||
|
}
|
||||||
|
const type =
|
||||||
|
appState.activeTool.type !== "custom" &&
|
||||||
|
isValidSubtype(subtype, appState.activeTool.type)
|
||||||
|
? appState.activeTool.type
|
||||||
|
: parentType;
|
||||||
|
const activeTool = !inactive
|
||||||
|
? appState.activeTool
|
||||||
|
: updateActiveTool(appState, { type });
|
||||||
|
const selectedElementIds = activated ? {} : appState.selectedElementIds;
|
||||||
|
const selectedGroupIds = activated ? {} : appState.selectedGroupIds;
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
activeSubtypes,
|
||||||
|
selectedElementIds,
|
||||||
|
selectedGroupIds,
|
||||||
|
activeTool,
|
||||||
|
},
|
||||||
|
storeAction: "capture",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest,
|
||||||
|
PanelComponent: ({ elements, appState, updateData, data }) => (
|
||||||
|
<button
|
||||||
|
className={clsx("ToolIcon_type_button", "ToolIcon_type_button--show", {
|
||||||
|
ToolIcon: true,
|
||||||
|
"ToolIcon--selected":
|
||||||
|
appState.activeSubtypes !== undefined &&
|
||||||
|
appState.activeSubtypes.includes(subtype),
|
||||||
|
"ToolIcon--plain": true,
|
||||||
|
})}
|
||||||
|
title={`${t(`toolBar.${subtype}`)}${title}`}
|
||||||
|
aria-label={t(`toolBar.${subtype}`)}
|
||||||
|
onClick={() => {
|
||||||
|
updateData(null);
|
||||||
|
}}
|
||||||
|
onContextMenu={
|
||||||
|
data && "onContextMenu" in data
|
||||||
|
? (event: React.MouseEvent) => {
|
||||||
|
if (
|
||||||
|
appState.activeSubtypes === undefined ||
|
||||||
|
(appState.activeSubtypes !== undefined &&
|
||||||
|
!appState.activeSubtypes.includes(subtype))
|
||||||
|
) {
|
||||||
|
updateData(null);
|
||||||
|
}
|
||||||
|
data.onContextMenu(event, subtype);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<div className="ToolIcon__icon" aria-hidden="true">
|
||||||
|
{icon.call(this, { theme: appState.theme })}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (key === "") {
|
||||||
|
delete subtypeAction.keyTest;
|
||||||
|
}
|
||||||
|
return subtypeAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubtypeToggles = () => {
|
||||||
|
const am = useExcalidrawActionManager();
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const onContextMenu = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
subtype: string,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { top: offsetTop, left: offsetLeft } =
|
||||||
|
container!.getBoundingClientRect();
|
||||||
|
const left = event.clientX - offsetLeft;
|
||||||
|
const top = event.clientY - offsetTop;
|
||||||
|
|
||||||
|
const items: ContextMenuItems = [];
|
||||||
|
am.filterActions(isSubtypeAction).forEach(
|
||||||
|
(action) =>
|
||||||
|
am.isActionEnabled(action, { data: { subtype } }) && items.push(action),
|
||||||
|
);
|
||||||
|
setAppState({}, () => {
|
||||||
|
setAppState({
|
||||||
|
contextMenu: { top, left, items },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only render if one or more subtypes are registered
|
||||||
|
if (getSubtypeNames().length === 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Island
|
||||||
|
style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
alignSelf: "center",
|
||||||
|
height: "fit-content",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getSubtypeNames().map((subtype) =>
|
||||||
|
am.renderAction(
|
||||||
|
makeCustomActionName(subtype),
|
||||||
|
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Island>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SubtypeToggles.displayName = "SubtypeToggles";
|
||||||
|
|
||||||
|
export const SubtypeShapeActions = (props: {
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
}) => {
|
||||||
|
const am = useExcalidrawActionManager();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{am
|
||||||
|
.filterActions(isSubtypeAction, { elements: props.elements })
|
||||||
|
.map((action) => am.renderAction(action.name))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SubtypeShapeActions.displayName = "SubtypeShapeActions";
|
||||||
@@ -36,7 +36,7 @@ import { trackEvent } from "../../analytics";
|
|||||||
import { useAppProps, useExcalidrawAppState } from "../App";
|
import { useAppProps, useExcalidrawAppState } from "../App";
|
||||||
import { isEmbeddableElement } from "../../element/typeChecks";
|
import { isEmbeddableElement } from "../../element/typeChecks";
|
||||||
import { getLinkHandleFromCoords } from "./helpers";
|
import { getLinkHandleFromCoords } from "./helpers";
|
||||||
import { point, type GlobalPoint } from "../../../math";
|
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@@ -181,7 +181,7 @@ export const Hyperlink = ({
|
|||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
point(event.clientX, event.clientY),
|
pointFrom(event.clientX, event.clientY),
|
||||||
) as boolean;
|
) as boolean;
|
||||||
if (shouldHide) {
|
if (shouldHide) {
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GlobalPoint, Radians } from "../../../math";
|
import type { GlobalPoint, Radians } from "../../../math";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { pointFrom, pointRotateRads } from "../../../math";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import type { Bounds } from "../../element/bounds";
|
import type { Bounds } from "../../element/bounds";
|
||||||
import { getElementAbsoluteCoords } from "../../element/bounds";
|
import { getElementAbsoluteCoords } from "../../element/bounds";
|
||||||
@@ -35,8 +35,8 @@ export const getLinkHandleFromCoords = (
|
|||||||
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
||||||
|
|
||||||
const [rotatedX, rotatedY] = pointRotateRads(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
point(x + linkWidth / 2, y + linkHeight / 2),
|
pointFrom(x + linkWidth / 2, y + linkHeight / 2),
|
||||||
point(centerX, centerY),
|
pointFrom(centerX, centerY),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
@@ -85,5 +85,10 @@ export const isPointHittingLink = (
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y));
|
return isPointHittingLinkIcon(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
pointFrom(x, y),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { isFiniteNumber, point } from "../../math";
|
import { isFiniteNumber, pointFrom } from "../../math";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -121,7 +121,8 @@ const repairBinding = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||||
|
subtype?: ExcalidrawElement["subtype"];
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
@@ -184,6 +185,9 @@ const restoreElementWithProperties = <
|
|||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ("subtype" in element) {
|
||||||
|
base.subtype = element.subtype;
|
||||||
|
}
|
||||||
if ("customData" in element || "customData" in extra) {
|
if ("customData" in element || "customData" in extra) {
|
||||||
base.customData =
|
base.customData =
|
||||||
"customData" in extra ? extra.customData : element.customData;
|
"customData" in extra ? extra.customData : element.customData;
|
||||||
@@ -268,7 +272,7 @@ const restoreElement = (
|
|||||||
let y = element.y;
|
let y = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points = // migrate old arrow model to new one
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
? [point(0, 0), point(element.width, element.height)]
|
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
||||||
: element.points;
|
: element.points;
|
||||||
|
|
||||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
@@ -296,7 +300,7 @@ const restoreElement = (
|
|||||||
let y: number | undefined = element.y;
|
let y: number | undefined = element.y;
|
||||||
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
? [point(0, 0), point(element.width, element.height)]
|
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
||||||
: element.points;
|
: element.points;
|
||||||
|
|
||||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
@@ -597,6 +601,12 @@ export const restoreAppState = (
|
|||||||
: defaultValue;
|
: defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("activeSubtypes" in appState) {
|
||||||
|
nextAppState.activeSubtypes = appState.activeSubtypes;
|
||||||
|
}
|
||||||
|
if ("customData" in appState) {
|
||||||
|
nextAppState.customData = appState.customData;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
cursorButton: localAppState?.cursorButton || "up",
|
cursorButton: localAppState?.cursorButton || "up",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { vi } from "vitest";
|
|||||||
import type { ExcalidrawElementSkeleton } from "./transform";
|
import type { ExcalidrawElementSkeleton } from "./transform";
|
||||||
import { convertToExcalidrawElements } from "./transform";
|
import { convertToExcalidrawElements } from "./transform";
|
||||||
import type { ExcalidrawArrowElement } from "../element/types";
|
import type { ExcalidrawArrowElement } from "../element/types";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const opts = { regenerateIds: false };
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
@@ -917,7 +917,7 @@ describe("Test Transform", () => {
|
|||||||
x: 111.262,
|
x: 111.262,
|
||||||
y: 57,
|
y: 57,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [point(0, 0), point(272.985, 0)],
|
points: [pointFrom(0, 0), pointFrom(272.985, 0)],
|
||||||
label: {
|
label: {
|
||||||
text: "How are you?",
|
text: "How are you?",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
@@ -940,7 +940,7 @@ describe("Test Transform", () => {
|
|||||||
x: 77.017,
|
x: 77.017,
|
||||||
y: 79,
|
y: 79,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [point(0, 0)],
|
points: [pointFrom(0, 0)],
|
||||||
label: {
|
label: {
|
||||||
text: "Friendship",
|
text: "Friendship",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import { randomId } from "../random";
|
|||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
import { isArrowElement } from "../element/typeChecks";
|
import { isArrowElement } from "../element/typeChecks";
|
||||||
import { point, type LocalPoint } from "../../math";
|
import { pointFrom, type LocalPoint } from "../../math";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -537,7 +537,7 @@ export const convertToExcalidrawElements = (
|
|||||||
excalidrawElement = newLinearElement({
|
excalidrawElement = newLinearElement({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
points: [point(0, 0), point(width, height)],
|
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ export const convertToExcalidrawElements = (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
points: [point(0, 0), point(width, height)],
|
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||||
...element,
|
...element,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import {
|
|||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
@@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(
|
vectorFromPoint(
|
||||||
p,
|
p,
|
||||||
point<GlobalPoint>(
|
pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + bindableElement.width / 2,
|
bindableElement.x + bindableElement.width / 2,
|
||||||
bindableElement.y + bindableElement.height / 2,
|
bindableElement.y + bindableElement.height / 2,
|
||||||
),
|
),
|
||||||
@@ -766,15 +766,15 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
const intersections = [
|
const intersections = [
|
||||||
...(intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
point(p[0], p[1] - 2 * bindableElement.height),
|
pointFrom(p[0], p[1] - 2 * bindableElement.height),
|
||||||
point(p[0], p[1] + 2 * bindableElement.height),
|
pointFrom(p[0], p[1] + 2 * bindableElement.height),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
) ?? []),
|
) ?? []),
|
||||||
...(intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
point(p[0] - 2 * bindableElement.width, p[1]),
|
pointFrom(p[0] - 2 * bindableElement.width, p[1]),
|
||||||
point(p[0] + 2 * bindableElement.width, p[1]),
|
pointFrom(p[0] + 2 * bindableElement.width, p[1]),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
) ?? []),
|
) ?? []),
|
||||||
@@ -815,25 +815,25 @@ const headingToMidBindPoint = (
|
|||||||
switch (true) {
|
switch (true) {
|
||||||
case compareHeading(heading, HEADING_UP):
|
case compareHeading(heading, HEADING_UP):
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
case compareHeading(heading, HEADING_RIGHT):
|
case compareHeading(heading, HEADING_RIGHT):
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
case compareHeading(heading, HEADING_DOWN):
|
case compareHeading(heading, HEADING_DOWN):
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
@@ -844,7 +844,7 @@ export const avoidRectangularCorner = (
|
|||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = point<GlobalPoint>(
|
const center = pointFrom<GlobalPoint>(
|
||||||
element.x + element.width / 2,
|
element.x + element.width / 2,
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
);
|
);
|
||||||
@@ -854,13 +854,13 @@ export const avoidRectangularCorner = (
|
|||||||
// Top left
|
// Top left
|
||||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
||||||
return pointRotateRads<GlobalPoint>(
|
return pointRotateRads<GlobalPoint>(
|
||||||
point(element.x - FIXED_BINDING_DISTANCE, element.y),
|
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(element.x, element.y - FIXED_BINDING_DISTANCE),
|
pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -871,13 +871,16 @@ export const avoidRectangularCorner = (
|
|||||||
// Bottom left
|
// Bottom left
|
||||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
|
pointFrom(
|
||||||
|
element.x,
|
||||||
|
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||||
|
),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -891,7 +894,7 @@ export const avoidRectangularCorner = (
|
|||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.x + element.width,
|
element.x + element.width,
|
||||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||||
),
|
),
|
||||||
@@ -900,7 +903,7 @@ export const avoidRectangularCorner = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.x + element.width + FIXED_BINDING_DISTANCE,
|
element.x + element.width + FIXED_BINDING_DISTANCE,
|
||||||
element.y + element.height,
|
element.y + element.height,
|
||||||
),
|
),
|
||||||
@@ -917,13 +920,16 @@ export const avoidRectangularCorner = (
|
|||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
|
pointFrom(
|
||||||
|
element.x + element.width,
|
||||||
|
element.y - FIXED_BINDING_DISTANCE,
|
||||||
|
),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -938,7 +944,10 @@ export const snapToMid = (
|
|||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
x + width / 2 - 0.1,
|
||||||
|
y + height / 2 - 0.1,
|
||||||
|
);
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
@@ -953,7 +962,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// LEFT
|
// LEFT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x - FIXED_BINDING_DISTANCE, center[1]),
|
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -964,7 +973,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// TOP
|
// TOP
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(center[0], y - FIXED_BINDING_DISTANCE),
|
pointFrom(center[0], y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -975,7 +984,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// RIGHT
|
// RIGHT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -986,7 +995,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// DOWN
|
// DOWN
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(center[0], y + height + FIXED_BINDING_DISTANCE),
|
pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -1023,11 +1032,11 @@ const updateBoundPoint = (
|
|||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = point<GlobalPoint>(
|
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + bindableElement.width / 2,
|
bindableElement.x + bindableElement.width / 2,
|
||||||
bindableElement.y + bindableElement.height / 2,
|
bindableElement.y + bindableElement.height / 2,
|
||||||
);
|
);
|
||||||
const global = point<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
);
|
);
|
||||||
@@ -1118,7 +1127,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
hoveredElement,
|
hoveredElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const globalMidPoint = point(
|
const globalMidPoint = pointFrom(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||||
);
|
);
|
||||||
@@ -1337,9 +1346,9 @@ export const bindingBorderTest = (
|
|||||||
const threshold = maxBindingGap(element, element.width, element.height);
|
const threshold = maxBindingGap(element, element.width, element.height);
|
||||||
const shape = getElementShape(element, elementsMap);
|
const shape = getElementShape(element, elementsMap);
|
||||||
return (
|
return (
|
||||||
isPointOnShape(point(x, y), shape, threshold) ||
|
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||||
(fullShape === true &&
|
(fullShape === true &&
|
||||||
pointInsideBounds(point(x, y), aabbForElement(element)))
|
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2197,11 +2206,11 @@ export const getGlobalFixedPointForBindableElement = (
|
|||||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||||
|
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
point<GlobalPoint>(
|
pointFrom<GlobalPoint>(
|
||||||
element.x + element.width / 2,
|
element.x + element.width / 2,
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
),
|
),
|
||||||
@@ -2229,7 +2238,7 @@ const getGlobalFixedPoints = (
|
|||||||
arrow.startBinding.fixedPoint,
|
arrow.startBinding.fixedPoint,
|
||||||
startElement as ExcalidrawBindableElement,
|
startElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: point<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[0][0],
|
arrow.x + arrow.points[0][0],
|
||||||
arrow.y + arrow.points[0][1],
|
arrow.y + arrow.points[0][1],
|
||||||
);
|
);
|
||||||
@@ -2239,7 +2248,7 @@ const getGlobalFixedPoints = (
|
|||||||
arrow.endBinding.fixedPoint,
|
arrow.endBinding.fixedPoint,
|
||||||
endElement as ExcalidrawBindableElement,
|
endElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: point<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||||
arrow.y + arrow.points[arrow.points.length - 1][1],
|
arrow.y + arrow.points[arrow.points.length - 1][1],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
@@ -125,9 +125,9 @@ describe("getElementBounds", () => {
|
|||||||
a: 0.6447741904932416,
|
a: 0.6447741904932416,
|
||||||
}),
|
}),
|
||||||
points: [
|
points: [
|
||||||
point<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
point<LocalPoint>(67.33984375, 92.48828125),
|
pointFrom<LocalPoint>(67.33984375, 92.48828125),
|
||||||
point<LocalPoint>(-102.7890625, 52.15625),
|
pointFrom<LocalPoint>(-102.7890625, 52.15625),
|
||||||
],
|
],
|
||||||
} as ExcalidrawLinearElement;
|
} as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
point,
|
pointFrom,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
@@ -113,8 +113,8 @@ export class ElementBounds {
|
|||||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||||
element.points.map(([x, y]) =>
|
element.points.map(([x, y]) =>
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(x, y),
|
pointFrom(x, y),
|
||||||
point(cx - element.x, cy - element.y),
|
pointFrom(cx - element.x, cy - element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -130,23 +130,23 @@ export class ElementBounds {
|
|||||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||||
} else if (element.type === "diamond") {
|
} else if (element.type === "diamond") {
|
||||||
const [x11, y11] = pointRotateRads(
|
const [x11, y11] = pointRotateRads(
|
||||||
point(cx, y1),
|
pointFrom(cx, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x12, y12] = pointRotateRads(
|
const [x12, y12] = pointRotateRads(
|
||||||
point(cx, y2),
|
pointFrom(cx, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x22, y22] = pointRotateRads(
|
const [x22, y22] = pointRotateRads(
|
||||||
point(x1, cy),
|
pointFrom(x1, cy),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x21, y21] = pointRotateRads(
|
const [x21, y21] = pointRotateRads(
|
||||||
point(x2, cy),
|
pointFrom(x2, cy),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
@@ -164,23 +164,23 @@ export class ElementBounds {
|
|||||||
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||||
} else {
|
} else {
|
||||||
const [x11, y11] = pointRotateRads(
|
const [x11, y11] = pointRotateRads(
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x12, y12] = pointRotateRads(
|
const [x12, y12] = pointRotateRads(
|
||||||
point(x1, y2),
|
pointFrom(x1, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x22, y22] = pointRotateRads(
|
const [x22, y22] = pointRotateRads(
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x21, y21] = pointRotateRads(
|
const [x21, y21] = pointRotateRads(
|
||||||
point(x2, y1),
|
pointFrom(x2, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
@@ -255,7 +255,7 @@ export const getElementLineSegments = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center: GlobalPoint = point(cx, cy);
|
const center: GlobalPoint = pointFrom(cx, cy);
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
@@ -266,7 +266,7 @@ export const getElementLineSegments = (
|
|||||||
segments.push(
|
segments.push(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.points[i][0] + element.x,
|
element.points[i][0] + element.x,
|
||||||
element.points[i][1] + element.y,
|
element.points[i][1] + element.y,
|
||||||
),
|
),
|
||||||
@@ -274,7 +274,7 @@ export const getElementLineSegments = (
|
|||||||
element.angle,
|
element.angle,
|
||||||
),
|
),
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.points[i + 1][0] + element.x,
|
element.points[i + 1][0] + element.x,
|
||||||
element.points[i + 1][1] + element.y,
|
element.points[i + 1][1] + element.y,
|
||||||
),
|
),
|
||||||
@@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = (
|
|||||||
ops: Op[],
|
ops: Op[],
|
||||||
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
let currentP: GlobalPoint = point(0, 0);
|
let currentP: GlobalPoint = pointFrom(0, 0);
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
(limits, { op, data }) => {
|
(limits, { op, data }) => {
|
||||||
@@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
|||||||
// move operation does not draw anything; so, it always
|
// move operation does not draw anything; so, it always
|
||||||
// returns false
|
// returns false
|
||||||
} else if (op === "bcurveTo") {
|
} else if (op === "bcurveTo") {
|
||||||
const _p1 = point<GlobalPoint>(data[0], data[1]);
|
const _p1 = pointFrom<GlobalPoint>(data[0], data[1]);
|
||||||
const _p2 = point<GlobalPoint>(data[2], data[3]);
|
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
|
||||||
const _p3 = point<GlobalPoint>(data[4], data[5]);
|
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
|
||||||
|
|
||||||
const p1 = transformXY ? transformXY(_p1) : _p1;
|
const p1 = transformXY ? transformXY(_p1) : _p1;
|
||||||
const p2 = transformXY ? transformXY(_p2) : _p2;
|
const p2 = transformXY ? transformXY(_p2) : _p2;
|
||||||
@@ -591,21 +591,21 @@ export const getArrowheadPoints = (
|
|||||||
|
|
||||||
invariant(data.length === 6, "Op data length is not 6");
|
invariant(data.length === 6, "Op data length is not 6");
|
||||||
|
|
||||||
const p3 = point(data[4], data[5]);
|
const p3 = pointFrom(data[4], data[5]);
|
||||||
const p2 = point(data[2], data[3]);
|
const p2 = pointFrom(data[2], data[3]);
|
||||||
const p1 = point(data[0], data[1]);
|
const p1 = pointFrom(data[0], data[1]);
|
||||||
|
|
||||||
// We need to find p0 of the bezier curve.
|
// We need to find p0 of the bezier curve.
|
||||||
// It is typically the last point of the previous
|
// It is typically the last point of the previous
|
||||||
// curve; it can also be the position of moveTo operation.
|
// curve; it can also be the position of moveTo operation.
|
||||||
const prevOp = ops[index - 1];
|
const prevOp = ops[index - 1];
|
||||||
let p0 = point(0, 0);
|
let p0 = pointFrom(0, 0);
|
||||||
if (prevOp.op === "move") {
|
if (prevOp.op === "move") {
|
||||||
const p = pointFromArray(prevOp.data);
|
const p = pointFromArray(prevOp.data);
|
||||||
invariant(p != null, "Op data is not a point");
|
invariant(p != null, "Op data is not a point");
|
||||||
p0 = p;
|
p0 = p;
|
||||||
} else if (prevOp.op === "bcurveTo") {
|
} else if (prevOp.op === "bcurveTo") {
|
||||||
p0 = point(prevOp.data[4], prevOp.data[5]);
|
p0 = pointFrom(prevOp.data[4], prevOp.data[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||||
@@ -671,13 +671,13 @@ export const getArrowheadPoints = (
|
|||||||
|
|
||||||
// Return points
|
// Return points
|
||||||
const [x3, y3] = pointRotateRads(
|
const [x3, y3] = pointRotateRads(
|
||||||
point(xs, ys),
|
pointFrom(xs, ys),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
((-angle * Math.PI) / 180) as Radians,
|
((-angle * Math.PI) / 180) as Radians,
|
||||||
);
|
);
|
||||||
const [x4, y4] = pointRotateRads(
|
const [x4, y4] = pointRotateRads(
|
||||||
point(xs, ys),
|
pointFrom(xs, ys),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
degreesToRadians(angle),
|
degreesToRadians(angle),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -690,8 +690,8 @@ export const getArrowheadPoints = (
|
|||||||
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||||
|
|
||||||
[ox, oy] = pointRotateRads(
|
[ox, oy] = pointRotateRads(
|
||||||
point(x2 + minSize * 2, y2),
|
pointFrom(x2 + minSize * 2, y2),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
Math.atan2(py - y2, px - x2) as Radians,
|
Math.atan2(py - y2, px - x2) as Radians,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -701,8 +701,8 @@ export const getArrowheadPoints = (
|
|||||||
: [0, 0];
|
: [0, 0];
|
||||||
|
|
||||||
[ox, oy] = pointRotateRads(
|
[ox, oy] = pointRotateRads(
|
||||||
point(x2 - minSize * 2, y2),
|
pointFrom(x2 - minSize * 2, y2),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
Math.atan2(y2 - py, x2 - px) as Radians,
|
Math.atan2(y2 - py, x2 - px) as Radians,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = (
|
|||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
point(element.x + pointX, element.y + pointY),
|
pointFrom(element.x + pointX, element.y + pointY),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = (
|
|||||||
const ops = getCurvePathOps(shape);
|
const ops = getCurvePathOps(shape);
|
||||||
const transformXY = ([x, y]: GlobalPoint) =>
|
const transformXY = ([x, y]: GlobalPoint) =>
|
||||||
pointRotateRads<GlobalPoint>(
|
pointRotateRads<GlobalPoint>(
|
||||||
point(element.x + x, element.y + y),
|
pointFrom(element.x + x, element.y + y),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
@@ -931,8 +931,8 @@ export const getClosestElementBounds = (
|
|||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
point((x1 + x2) / 2, (y1 + y2) / 2),
|
pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
|
||||||
point(from.x, from.y),
|
pointFrom(from.x, from.y),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
@@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
||||||
point(
|
pointFrom(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||||
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
||||||
import { isPointWithinBounds, point } from "../../math";
|
import { isPointWithinBounds, pointFrom } from "../../math";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
@@ -61,13 +61,13 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
|||||||
let hit = shouldTestInside(element)
|
let hit = shouldTestInside(element)
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||||
// we would need `onShape` as well to include the "borders"
|
// we would need `onShape` as well to include the "borders"
|
||||||
isPointInShape(point(x, y), shape) ||
|
isPointInShape(pointFrom(x, y), shape) ||
|
||||||
isPointOnShape(point(x, y), shape, threshold)
|
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||||
: isPointOnShape(point(x, y), shape, threshold);
|
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
||||||
|
|
||||||
// hit test against a frame's name
|
// hit test against a frame's name
|
||||||
if (!hit && frameNameBound) {
|
if (!hit && frameNameBound) {
|
||||||
hit = isPointInShape(point(x, y), {
|
hit = isPointInShape(pointFrom(x, y), {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||||
.data as Polygon<Point>,
|
.data as Polygon<Point>,
|
||||||
@@ -89,7 +89,11 @@ export const hitElementBoundingBox = (
|
|||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
return isPointWithinBounds(
|
||||||
|
pointFrom(x1, y1),
|
||||||
|
pointFrom(x, y),
|
||||||
|
pointFrom(x2, y2),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = <
|
export const hitElementBoundingBoxOnly = <
|
||||||
@@ -115,5 +119,5 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
y: number,
|
y: number,
|
||||||
textShape: GeometricShape<Point> | null,
|
textShape: GeometricShape<Point> | null,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape(point(x, y), textShape);
|
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { invariant } from "../utils";
|
import { invariant } from "../utils";
|
||||||
import { point, type LocalPoint } from "../../math";
|
import { pointFrom, type LocalPoint } from "../../math";
|
||||||
import { aabbForElement } from "../shapes";
|
import { aabbForElement } from "../shapes";
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
@@ -421,7 +421,7 @@ const createBindingArrow = (
|
|||||||
strokeColor: appState.currentItemStrokeColor,
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
strokeWidth: appState.currentItemStrokeWidth,
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
points: [point(0, 0), point(endX, endY)],
|
points: [pointFrom(0, 0), pointFrom(endX, endY)],
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
Radians,
|
Radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointScaleFromOrigin,
|
pointScaleFromOrigin,
|
||||||
radiansToDegrees,
|
radiansToDegrees,
|
||||||
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
|
|||||||
|
|
||||||
const top = pointRotateRads(
|
const top = pointRotateRads(
|
||||||
pointScaleFromOrigin(
|
pointScaleFromOrigin(
|
||||||
point(element.x + element.width / 2, element.y),
|
pointFrom(element.x + element.width / 2, element.y),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
@@ -91,7 +91,7 @@ export const headingForPointFromElement = <
|
|||||||
);
|
);
|
||||||
const right = pointRotateRads(
|
const right = pointRotateRads(
|
||||||
pointScaleFromOrigin(
|
pointScaleFromOrigin(
|
||||||
point(element.x + element.width, element.y + element.height / 2),
|
pointFrom(element.x + element.width, element.y + element.height / 2),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
@@ -100,7 +100,7 @@ export const headingForPointFromElement = <
|
|||||||
);
|
);
|
||||||
const bottom = pointRotateRads(
|
const bottom = pointRotateRads(
|
||||||
pointScaleFromOrigin(
|
pointScaleFromOrigin(
|
||||||
point(element.x + element.width / 2, element.y + element.height),
|
pointFrom(element.x + element.width / 2, element.y + element.height),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
@@ -109,7 +109,7 @@ export const headingForPointFromElement = <
|
|||||||
);
|
);
|
||||||
const left = pointRotateRads(
|
const left = pointRotateRads(
|
||||||
pointScaleFromOrigin(
|
pointScaleFromOrigin(
|
||||||
point(element.x, element.y + element.height / 2),
|
pointFrom(element.x, element.y + element.height / 2),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
@@ -133,22 +133,22 @@ export const headingForPointFromElement = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const topLeft = pointScaleFromOrigin(
|
const topLeft = pointScaleFromOrigin(
|
||||||
point(aabb[0], aabb[1]),
|
pointFrom(aabb[0], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const topRight = pointScaleFromOrigin(
|
const topRight = pointScaleFromOrigin(
|
||||||
point(aabb[2], aabb[1]),
|
pointFrom(aabb[2], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const bottomLeft = pointScaleFromOrigin(
|
const bottomLeft = pointScaleFromOrigin(
|
||||||
point(aabb[0], aabb[3]),
|
pointFrom(aabb[0], aabb[3]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const bottomRight = pointScaleFromOrigin(
|
const bottomRight = pointScaleFromOrigin(
|
||||||
point(aabb[2], aabb[3]),
|
pointFrom(aabb[2], aabb[3]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import type Scene from "../scene/Scene";
|
|||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
pointCenter,
|
pointCenter,
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
vector,
|
vector,
|
||||||
@@ -108,7 +108,7 @@ export class LinearElementEditor {
|
|||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], point(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
referencePoint,
|
referencePoint,
|
||||||
point(scenePointerX, scenePointerY),
|
pointFrom(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ export class LinearElementEditor {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
point: point(
|
point: pointFrom(
|
||||||
width + referencePoint[0],
|
width + referencePoint[0],
|
||||||
height + referencePoint[1],
|
height + referencePoint[1],
|
||||||
),
|
),
|
||||||
@@ -329,7 +329,7 @@ export class LinearElementEditor {
|
|||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
)
|
)
|
||||||
: point(
|
: pointFrom(
|
||||||
element.points[pointIndex][0] + deltaX,
|
element.points[pointIndex][0] + deltaX,
|
||||||
element.points[pointIndex][1] + deltaY,
|
element.points[pointIndex][1] + deltaY,
|
||||||
);
|
);
|
||||||
@@ -590,11 +590,11 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor.segmentMidPointHoveredCoords;
|
linearElementEditor.segmentMidPointHoveredCoords;
|
||||||
if (existingSegmentMidpointHitCoords) {
|
if (existingSegmentMidpointHitCoords) {
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
point(
|
pointFrom(
|
||||||
existingSegmentMidpointHitCoords[0],
|
existingSegmentMidpointHitCoords[0],
|
||||||
existingSegmentMidpointHitCoords[1],
|
existingSegmentMidpointHitCoords[1],
|
||||||
),
|
),
|
||||||
point(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
if (distance <= threshold) {
|
if (distance <= threshold) {
|
||||||
return existingSegmentMidpointHitCoords;
|
return existingSegmentMidpointHitCoords;
|
||||||
@@ -606,8 +606,8 @@ export class LinearElementEditor {
|
|||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (midPoints[index] !== null) {
|
if (midPoints[index] !== null) {
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
point(midPoints[index]![0], midPoints[index]![1]),
|
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
||||||
point(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
if (distance <= threshold) {
|
if (distance <= threshold) {
|
||||||
return midPoints[index];
|
return midPoints[index];
|
||||||
@@ -626,8 +626,8 @@ export class LinearElementEditor {
|
|||||||
zoom: AppState["zoom"],
|
zoom: AppState["zoom"],
|
||||||
) {
|
) {
|
||||||
let distance = pointDistance(
|
let distance = pointDistance(
|
||||||
point(startPoint[0], startPoint[1]),
|
pointFrom(startPoint[0], startPoint[1]),
|
||||||
point(endPoint[0], endPoint[1]),
|
pointFrom(endPoint[0], endPoint[1]),
|
||||||
);
|
);
|
||||||
if (element.points.length > 2 && element.roundness) {
|
if (element.points.length > 2 && element.roundness) {
|
||||||
distance = getBezierCurveLength(element, endPoint);
|
distance = getBezierCurveLength(element, endPoint);
|
||||||
@@ -829,11 +829,11 @@ export class LinearElementEditor {
|
|||||||
const targetPoint =
|
const targetPoint =
|
||||||
clickedPointIndex > -1 &&
|
clickedPointIndex > -1 &&
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(
|
pointFrom(
|
||||||
element.x + element.points[clickedPointIndex][0],
|
element.x + element.points[clickedPointIndex][0],
|
||||||
element.y + element.points[clickedPointIndex][1],
|
element.y + element.points[clickedPointIndex][1],
|
||||||
),
|
),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -928,11 +928,11 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
point(scenePointerX, scenePointerY),
|
pointFrom(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
newPoint = point(
|
newPoint = pointFrom(
|
||||||
width + lastCommittedPoint[0],
|
width + lastCommittedPoint[0],
|
||||||
height + lastCommittedPoint[1],
|
height + lastCommittedPoint[1],
|
||||||
);
|
);
|
||||||
@@ -984,8 +984,8 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x + p[0], y + p[1]),
|
pointFrom(x + p[0], y + p[1]),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1001,8 +1001,8 @@ export class LinearElementEditor {
|
|||||||
return element.points.map((p) => {
|
return element.points.map((p) => {
|
||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x + p[0], y + p[1]),
|
pointFrom(x + p[0], y + p[1]),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1025,8 +1025,12 @@ export class LinearElementEditor {
|
|||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
|
|
||||||
return p
|
return p
|
||||||
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
|
? pointRotateRads(
|
||||||
: pointRotateRads(point(x, y), point(cx, cy), element.angle);
|
pointFrom(x + p[0], y + p[1]),
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
)
|
||||||
|
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pointFromAbsoluteCoords(
|
static pointFromAbsoluteCoords(
|
||||||
@@ -1036,7 +1040,7 @@ export class LinearElementEditor {
|
|||||||
): LocalPoint {
|
): LocalPoint {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
// No rotation for elbow arrows
|
// No rotation for elbow arrows
|
||||||
return point(
|
return pointFrom(
|
||||||
absoluteCoords[0] - element.x,
|
absoluteCoords[0] - element.x,
|
||||||
absoluteCoords[1] - element.y,
|
absoluteCoords[1] - element.y,
|
||||||
);
|
);
|
||||||
@@ -1046,11 +1050,11 @@ export class LinearElementEditor {
|
|||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
point(absoluteCoords[0], absoluteCoords[1]),
|
pointFrom(absoluteCoords[0], absoluteCoords[1]),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
return point(x - element.x, y - element.y);
|
return pointFrom(x - element.x, y - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPointIndexUnderCursor(
|
static getPointIndexUnderCursor(
|
||||||
@@ -1071,7 +1075,7 @@ export class LinearElementEditor {
|
|||||||
while (--idx > -1) {
|
while (--idx > -1) {
|
||||||
const p = pointHandles[idx];
|
const p = pointHandles[idx];
|
||||||
if (
|
if (
|
||||||
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
|
pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value <
|
||||||
// +1px to account for outline stroke
|
// +1px to account for outline stroke
|
||||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||||
) {
|
) {
|
||||||
@@ -1093,12 +1097,12 @@ export class LinearElementEditor {
|
|||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const [rotatedX, rotatedY] = pointRotateRads(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
point(pointerOnGrid[0], pointerOnGrid[1]),
|
pointFrom(pointerOnGrid[0], pointerOnGrid[1]),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
return point(rotatedX - element.x, rotatedY - element.y);
|
return pointFrom(rotatedX - element.x, rotatedY - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1118,7 +1122,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
points: points.map((p) => {
|
points: points.map((p) => {
|
||||||
return point(p[0] - offsetX, p[1] - offsetY);
|
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
||||||
}),
|
}),
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
@@ -1172,8 +1176,8 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
acc.push(
|
acc.push(
|
||||||
nextPoint
|
nextPoint
|
||||||
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||||
: point(p[0], p[1]),
|
: pointFrom(p[0], p[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
nextSelectedIndices.push(indexCursor + 1);
|
nextSelectedIndices.push(indexCursor + 1);
|
||||||
@@ -1194,7 +1198,7 @@ export class LinearElementEditor {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: point(lastPoint[0] + 30, lastPoint[1] + 30),
|
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -1235,7 +1239,9 @@ export class LinearElementEditor {
|
|||||||
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
||||||
if (!pointIndices.includes(idx)) {
|
if (!pointIndices.includes(idx)) {
|
||||||
acc.push(
|
acc.push(
|
||||||
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
|
!acc.length
|
||||||
|
? pointFrom(0, 0)
|
||||||
|
: pointFrom(p[0] - offsetX, p[1] - offsetY),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1312,9 +1318,9 @@ export class LinearElementEditor {
|
|||||||
const deltaY =
|
const deltaY =
|
||||||
selectedPointData.point[1] - points[selectedPointData.index][1];
|
selectedPointData.point[1] - points[selectedPointData.index][1];
|
||||||
|
|
||||||
return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
||||||
}
|
}
|
||||||
return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p;
|
return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p;
|
||||||
});
|
});
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
@@ -1368,8 +1374,8 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const origin = linearElementEditor.pointerDownState.origin!;
|
const origin = linearElementEditor.pointerDownState.origin!;
|
||||||
const dist = pointDistance(
|
const dist = pointDistance(
|
||||||
point(origin.x, origin.y),
|
pointFrom(origin.x, origin.y),
|
||||||
point(pointerCoords.x, pointerCoords.y),
|
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
@@ -1493,8 +1499,8 @@ export class LinearElementEditor {
|
|||||||
const dX = prevCenterX - nextCenterX;
|
const dX = prevCenterX - nextCenterX;
|
||||||
const dY = prevCenterY - nextCenterY;
|
const dY = prevCenterY - nextCenterY;
|
||||||
const rotated = pointRotateRads(
|
const rotated = pointRotateRads(
|
||||||
point(offsetX, offsetY),
|
pointFrom(offsetX, offsetY),
|
||||||
point(dX, dY),
|
pointFrom(dX, dY),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -1540,8 +1546,8 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(width, height),
|
pointFrom(width, height),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1611,36 +1617,36 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
||||||
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
||||||
const centerPoint = point(cx, cy);
|
const centerPoint = pointFrom(cx, cy);
|
||||||
|
|
||||||
const topLeftRotatedPoint = pointRotateRads(
|
const topLeftRotatedPoint = pointRotateRads(
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topRightRotatedPoint = pointRotateRads(
|
const topRightRotatedPoint = pointRotateRads(
|
||||||
point(x2, y1),
|
pointFrom(x2, y1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
const counterRotateBoundTextTopLeft = pointRotateRads(
|
const counterRotateBoundTextTopLeft = pointRotateRads(
|
||||||
point(boundTextX1, boundTextY1),
|
pointFrom(boundTextX1, boundTextY1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextTopRight = pointRotateRads(
|
const counterRotateBoundTextTopRight = pointRotateRads(
|
||||||
point(boundTextX2, boundTextY1),
|
pointFrom(boundTextX2, boundTextY1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
||||||
point(boundTextX1, boundTextY2),
|
pointFrom(boundTextX1, boundTextY2),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomRight = pointRotateRads(
|
const counterRotateBoundTextBottomRight = pointRotateRads(
|
||||||
point(boundTextX2, boundTextY2),
|
pointFrom(boundTextX2, boundTextY2),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,12 +5,23 @@ import { randomInteger } from "../random";
|
|||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import { maybeGetSubtypeProps } from "./newElement";
|
||||||
|
import { getSubtypeMethods } from "./subtypes";
|
||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
"id" | "version" | "versionNonce" | "updated"
|
"id" | "version" | "versionNonce" | "updated"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const cleanUpdates = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
): ElementUpdate<TElement> => {
|
||||||
|
const subtype = maybeGetSubtypeProps(element, element.type).subtype;
|
||||||
|
const map = getSubtypeMethods(subtype);
|
||||||
|
return map?.clean ? (map.clean(updates) as typeof updates) : updates;
|
||||||
|
};
|
||||||
|
|
||||||
// This function tracks updates of text elements for the purposes for collaboration.
|
// This function tracks updates of text elements for the purposes for collaboration.
|
||||||
// The version is used to compare updates when more than one user is working in
|
// The version is used to compare updates when more than one user is working in
|
||||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||||
@@ -21,6 +32,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
informMutation = true,
|
informMutation = true,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
let increment = false;
|
||||||
|
const oldUpdates = cleanUpdates(element, updates);
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
@@ -69,6 +82,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!didChangePoints) {
|
if (!didChangePoints) {
|
||||||
|
key in oldUpdates && (increment = true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +90,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
|
|
||||||
(element as any)[key] = value;
|
(element as any)[key] = value;
|
||||||
didChange = true;
|
didChange = true;
|
||||||
|
key in oldUpdates && (increment = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +107,11 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
element.version++;
|
if (increment) {
|
||||||
element.versionNonce = randomInteger();
|
element.version++;
|
||||||
element.updated = getUpdatedTimestamp();
|
element.versionNonce = randomInteger();
|
||||||
|
element.updated = getUpdatedTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
if (informMutation) {
|
if (informMutation) {
|
||||||
Scene.getScene(element)?.triggerUpdate();
|
Scene.getScene(element)?.triggerUpdate();
|
||||||
@@ -110,6 +127,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
force = false,
|
force = false,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
let increment = false;
|
||||||
|
const oldUpdates = cleanUpdates(element, updates);
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
const value = (updates as any)[key];
|
const value = (updates as any)[key];
|
||||||
if (typeof value !== "undefined") {
|
if (typeof value !== "undefined") {
|
||||||
@@ -121,6 +140,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
didChange = true;
|
didChange = true;
|
||||||
|
key in oldUpdates && (increment = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +148,9 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!increment) {
|
||||||
|
return { ...element, ...updates };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
...updates,
|
...updates,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants";
|
|||||||
import { isPrimitive } from "../utils";
|
import { isPrimitive } from "../utils";
|
||||||
import type { ExcalidrawLinearElement } from "./types";
|
import type { ExcalidrawLinearElement } from "./types";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const assertCloneObjects = (source: any, clone: any) => {
|
const assertCloneObjects = (source: any, clone: any) => {
|
||||||
for (const key in clone) {
|
for (const key in clone) {
|
||||||
@@ -38,7 +38,7 @@ describe("duplicating single elements", () => {
|
|||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)],
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element);
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||||
arrayToMap,
|
|
||||||
getFontString,
|
|
||||||
getUpdatedTimestamp,
|
|
||||||
isTestEnv,
|
|
||||||
} from "../utils";
|
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { bumpVersion, newElementWith } from "./mutateElement";
|
import { bumpVersion, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
@@ -32,9 +27,9 @@ import type { AppState } from "../types";
|
|||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
measureText,
|
measureTextElement,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapTextElement,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +43,30 @@ import {
|
|||||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
|
import { getSubtypeMethods, isValidSubtype } from "./subtypes";
|
||||||
|
|
||||||
|
export const maybeGetSubtypeProps = (
|
||||||
|
obj: {
|
||||||
|
subtype?: ExcalidrawElement["subtype"];
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
},
|
||||||
|
type: ExcalidrawElement["type"],
|
||||||
|
) => {
|
||||||
|
const data: typeof obj = {};
|
||||||
|
if ("subtype" in obj) {
|
||||||
|
data.subtype = obj.subtype;
|
||||||
|
}
|
||||||
|
if ("customData" in obj) {
|
||||||
|
data.customData = obj.customData;
|
||||||
|
}
|
||||||
|
if ("subtype" in data && !isValidSubtype(data.subtype, type)) {
|
||||||
|
delete data.subtype;
|
||||||
|
}
|
||||||
|
if (!("subtype" in data) && "customData" in data) {
|
||||||
|
delete data.customData;
|
||||||
|
}
|
||||||
|
return data as typeof obj;
|
||||||
|
};
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -62,6 +81,8 @@ export type ElementConstructorOpts = MarkOptional<
|
|||||||
| "version"
|
| "version"
|
||||||
| "versionNonce"
|
| "versionNonce"
|
||||||
| "link"
|
| "link"
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
| "strokeStyle"
|
| "strokeStyle"
|
||||||
| "fillStyle"
|
| "fillStyle"
|
||||||
| "strokeColor"
|
| "strokeColor"
|
||||||
@@ -99,8 +120,10 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
...rest
|
...rest
|
||||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||||
) => {
|
) => {
|
||||||
|
const { subtype, customData } = rest;
|
||||||
// assign type to guard against excess properties
|
// assign type to guard against excess properties
|
||||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||||
|
...maybeGetSubtypeProps({ subtype, customData }, type),
|
||||||
id: rest.id || randomId(),
|
id: rest.id || randomId(),
|
||||||
type,
|
type,
|
||||||
x,
|
x,
|
||||||
@@ -136,8 +159,11 @@ export const newElement = (
|
|||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawGenericElement["type"];
|
type: ExcalidrawGenericElement["type"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawGenericElement> =>
|
): NonDeleted<ExcalidrawGenericElement> => {
|
||||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
|
map?.clean && map.clean(opts);
|
||||||
|
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
|
};
|
||||||
|
|
||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
@@ -230,10 +256,12 @@ export const newTextElement = (
|
|||||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(
|
||||||
text,
|
{ ...opts, fontSize, fontFamily, lineHeight },
|
||||||
getFontString({ fontFamily, fontSize }),
|
{
|
||||||
lineHeight,
|
text,
|
||||||
|
customData: opts.customData,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||||
@@ -277,11 +305,9 @@ const getAdjustedDimensions = (
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
} => {
|
} => {
|
||||||
let { width: nextWidth, height: nextHeight } = measureText(
|
let { width: nextWidth, height: nextHeight } = measureTextElement(element, {
|
||||||
nextText,
|
text: nextText,
|
||||||
getFontString(element),
|
});
|
||||||
element.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// wrapped text
|
// wrapped text
|
||||||
if (!element.autoResize) {
|
if (!element.autoResize) {
|
||||||
@@ -297,11 +323,7 @@ const getAdjustedDimensions = (
|
|||||||
!element.containerId &&
|
!element.containerId &&
|
||||||
element.autoResize
|
element.autoResize
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureTextElement(element);
|
||||||
element.text,
|
|
||||||
getFontString(element),
|
|
||||||
element.lineHeight,
|
|
||||||
);
|
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
@@ -404,12 +426,14 @@ export const refreshTextDimensions = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (container || !textElement.autoResize) {
|
if (container || !textElement.autoResize) {
|
||||||
text = wrapText(
|
text = wrapTextElement(
|
||||||
text,
|
textElement,
|
||||||
getFontString(textElement),
|
|
||||||
container
|
container
|
||||||
? getBoundTextMaxWidth(container, textElement)
|
? getBoundTextMaxWidth(container, textElement)
|
||||||
: textElement.width,
|
: textElement.width,
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
|
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
|
||||||
@@ -424,6 +448,8 @@ export const newFreeDrawElement = (
|
|||||||
pressures?: ExcalidrawFreeDrawElement["pressures"];
|
pressures?: ExcalidrawFreeDrawElement["pressures"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||||
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
|
map?.clean && map.clean(opts);
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
@@ -439,6 +465,8 @@ export const newLinearElement = (
|
|||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
|
map?.clean && map.clean(opts);
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
@@ -459,6 +487,8 @@ export const newArrowElement = (
|
|||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawArrowElement> => {
|
): NonDeleted<ExcalidrawArrowElement> => {
|
||||||
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
|
map?.clean && map.clean(opts);
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
@@ -479,6 +509,8 @@ export const newImageElement = (
|
|||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawImageElement> => {
|
): NonDeleted<ExcalidrawImageElement> => {
|
||||||
|
const map = getSubtypeMethods(opts?.subtype);
|
||||||
|
map?.clean && map.clean(opts);
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
||||||
// in the future we'll support changing stroke color for some SVG elements,
|
// in the future we'll support changing stroke color for some SVG elements,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import type { GlobalPoint } from "../../math";
|
|||||||
import {
|
import {
|
||||||
pointCenter,
|
pointCenter,
|
||||||
normalizeRadians,
|
normalizeRadians,
|
||||||
point,
|
pointFrom,
|
||||||
pointFromPair,
|
pointFromPair,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type Radians,
|
type Radians,
|
||||||
@@ -240,8 +240,8 @@ const resizeSingleTextElement = (
|
|||||||
);
|
);
|
||||||
// rotation pointer with reverse angle
|
// rotation pointer with reverse angle
|
||||||
const [rotatedX, rotatedY] = pointRotateRads(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
point(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
let scaleX = 0;
|
let scaleX = 0;
|
||||||
@@ -276,23 +276,23 @@ const resizeSingleTextElement = (
|
|||||||
const startBottomRight = [x2, y2];
|
const startBottomRight = [x2, y2];
|
||||||
const startCenter = [cx, cy];
|
const startCenter = [cx, cy];
|
||||||
|
|
||||||
let newTopLeft = point<GlobalPoint>(x1, y1);
|
let newTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||||
if (["n", "w", "nw"].includes(transformHandleType)) {
|
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||||
newTopLeft = point<GlobalPoint>(
|
newTopLeft = pointFrom<GlobalPoint>(
|
||||||
startBottomRight[0] - Math.abs(nextWidth),
|
startBottomRight[0] - Math.abs(nextWidth),
|
||||||
startBottomRight[1] - Math.abs(nextHeight),
|
startBottomRight[1] - Math.abs(nextHeight),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (transformHandleType === "ne") {
|
if (transformHandleType === "ne") {
|
||||||
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||||
newTopLeft = point<GlobalPoint>(
|
newTopLeft = pointFrom<GlobalPoint>(
|
||||||
bottomLeft[0],
|
bottomLeft[0],
|
||||||
bottomLeft[1] - Math.abs(nextHeight),
|
bottomLeft[1] - Math.abs(nextHeight),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (transformHandleType === "sw") {
|
if (transformHandleType === "sw") {
|
||||||
const topRight = [startBottomRight[0], startTopLeft[1]];
|
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||||
newTopLeft = point<GlobalPoint>(
|
newTopLeft = pointFrom<GlobalPoint>(
|
||||||
topRight[0] - Math.abs(nextWidth),
|
topRight[0] - Math.abs(nextWidth),
|
||||||
topRight[1],
|
topRight[1],
|
||||||
);
|
);
|
||||||
@@ -311,12 +311,20 @@ const resizeSingleTextElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const angle = element.angle;
|
const angle = element.angle;
|
||||||
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
|
const rotatedTopLeft = pointRotateRads(
|
||||||
const newCenter = point<GlobalPoint>(
|
newTopLeft,
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
const newCenter = pointFrom<GlobalPoint>(
|
||||||
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
||||||
);
|
);
|
||||||
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
|
const rotatedNewCenter = pointRotateRads(
|
||||||
|
newCenter,
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
newTopLeft = pointRotateRads(
|
newTopLeft = pointRotateRads(
|
||||||
rotatedTopLeft,
|
rotatedTopLeft,
|
||||||
rotatedNewCenter,
|
rotatedNewCenter,
|
||||||
@@ -341,12 +349,12 @@ const resizeSingleTextElement = (
|
|||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft = point<GlobalPoint>(x1, y1);
|
const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||||
const startBottomRight = point<GlobalPoint>(x2, y2);
|
const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
|
||||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
point(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle as Radians,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
@@ -419,7 +427,7 @@ const resizeSingleTextElement = (
|
|||||||
startCenter,
|
startCenter,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
const newCenter = point(
|
const newCenter = pointFrom(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
);
|
);
|
||||||
@@ -461,13 +469,13 @@ export const resizeSingleElement = (
|
|||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft = point(x1, y1);
|
const startTopLeft = pointFrom(x1, y1);
|
||||||
const startBottomRight = point(x2, y2);
|
const startBottomRight = pointFrom(x2, y2);
|
||||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
// Calculate new dimensions based on cursor position
|
// Calculate new dimensions based on cursor position
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
point(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle as Radians,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
@@ -648,7 +656,7 @@ export const resizeSingleElement = (
|
|||||||
startCenter,
|
startCenter,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
const newCenter = point(
|
const newCenter = pointFrom(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
);
|
);
|
||||||
@@ -817,20 +825,20 @@ export const resizeMultipleElements = (
|
|||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
||||||
ne: point(minX, maxY),
|
ne: pointFrom(minX, maxY),
|
||||||
se: point(minX, minY),
|
se: pointFrom(minX, minY),
|
||||||
sw: point(maxX, minY),
|
sw: pointFrom(maxX, minY),
|
||||||
nw: point(maxX, maxY),
|
nw: pointFrom(maxX, maxY),
|
||||||
e: point(minX, minY + height / 2),
|
e: pointFrom(minX, minY + height / 2),
|
||||||
w: point(maxX, minY + height / 2),
|
w: pointFrom(maxX, minY + height / 2),
|
||||||
n: point(minX + width / 2, maxY),
|
n: pointFrom(minX + width / 2, maxY),
|
||||||
s: point(minX + width / 2, minY),
|
s: pointFrom(minX + width / 2, minY),
|
||||||
};
|
};
|
||||||
|
|
||||||
// anchor point must be on the opposite side of the dragged selection handle
|
// anchor point must be on the opposite side of the dragged selection handle
|
||||||
// or be the center of the selection if shouldResizeFromCenter
|
// or be the center of the selection if shouldResizeFromCenter
|
||||||
const [anchorX, anchorY] = shouldResizeFromCenter
|
const [anchorX, anchorY] = shouldResizeFromCenter
|
||||||
? point(midX, midY)
|
? pointFrom(midX, midY)
|
||||||
: anchorsMap[direction];
|
: anchorsMap[direction];
|
||||||
|
|
||||||
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||||
@@ -1044,8 +1052,8 @@ const rotateMultipleElements = (
|
|||||||
const origAngle =
|
const origAngle =
|
||||||
originalElements.get(element.id)?.angle ?? element.angle;
|
originalElements.get(element.id)?.angle ?? element.angle;
|
||||||
const [rotatedCX, rotatedCY] = pointRotateRads(
|
const [rotatedCX, rotatedCY] = pointRotateRads(
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
point(centerX, centerY),
|
pointFrom(centerX, centerY),
|
||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1101,40 +1109,44 @@ export const getResizeOffsetXY = (
|
|||||||
const angle = (
|
const angle = (
|
||||||
selectedElements.length === 1 ? selectedElements[0].angle : 0
|
selectedElements.length === 1 ? selectedElements[0].angle : 0
|
||||||
) as Radians;
|
) as Radians;
|
||||||
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
|
[x, y] = pointRotateRads(
|
||||||
|
pointFrom(x, y),
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
switch (transformHandleType) {
|
switch (transformHandleType) {
|
||||||
case "n":
|
case "n":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x - (x1 + x2) / 2, y - y1),
|
pointFrom(x - (x1 + x2) / 2, y - y1),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "s":
|
case "s":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x - (x1 + x2) / 2, y - y2),
|
pointFrom(x - (x1 + x2) / 2, y - y2),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "w":
|
case "w":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x - x1, y - (y1 + y2) / 2),
|
pointFrom(x - x1, y - (y1 + y2) / 2),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "e":
|
case "e":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
point(x - x2, y - (y1 + y2) / 2),
|
pointFrom(x - x2, y - (y1 + y2) / 2),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "nw":
|
case "nw":
|
||||||
return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle);
|
return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
|
||||||
case "ne":
|
case "ne":
|
||||||
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
|
return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
|
||||||
case "sw":
|
case "sw":
|
||||||
return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle);
|
return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
|
||||||
case "se":
|
case "se":
|
||||||
return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle);
|
return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
|
||||||
default:
|
default:
|
||||||
return [0, 0];
|
return [0, 0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
|||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
|
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type Radians,
|
type Radians,
|
||||||
@@ -92,16 +92,20 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
||||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
const sides = getSelectionBorders(
|
const sides = getSelectionBorders(
|
||||||
point(x1 - SPACING, y1 - SPACING),
|
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||||
point(x2 + SPACING, y2 + SPACING),
|
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [dir, side] of Object.entries(sides)) {
|
for (const [dir, side] of Object.entries(sides)) {
|
||||||
// test to see if x, y are on the line segment
|
// test to see if x, y are on the line segment
|
||||||
if (
|
if (
|
||||||
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
pointOnLineSegment(
|
||||||
|
pointFrom(x, y),
|
||||||
|
side as LineSegment<Point>,
|
||||||
|
SPACING,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return dir as TransformHandleType;
|
return dir as TransformHandleType;
|
||||||
}
|
}
|
||||||
@@ -178,9 +182,9 @@ export const getTransformHandleTypeFromCoords = <
|
|||||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
|
|
||||||
const sides = getSelectionBorders(
|
const sides = getSelectionBorders(
|
||||||
point(x1 - SPACING, y1 - SPACING),
|
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||||
point(x2 + SPACING, y2 + SPACING),
|
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
0 as Radians,
|
0 as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -188,7 +192,7 @@ export const getTransformHandleTypeFromCoords = <
|
|||||||
// test to see if x, y are on the line segment
|
// test to see if x, y are on the line segment
|
||||||
if (
|
if (
|
||||||
pointOnLineSegment(
|
pointOnLineSegment(
|
||||||
point(scenePointerX, scenePointerY),
|
pointFrom(scenePointerX, scenePointerY),
|
||||||
side as LineSegment<Point>,
|
side as LineSegment<Point>,
|
||||||
SPACING,
|
SPACING,
|
||||||
)
|
)
|
||||||
@@ -265,10 +269,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
center: Point,
|
center: Point,
|
||||||
angle: Radians,
|
angle: Radians,
|
||||||
) => {
|
) => {
|
||||||
const topLeft = pointRotateRads(point(x1, y1), center, angle);
|
const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
|
||||||
const topRight = pointRotateRads(point(x2, y1), center, angle);
|
const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
|
||||||
const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
|
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
|
||||||
const bottomRight = pointRotateRads(point(x2, y2), center, angle);
|
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
n: [topLeft, topRight],
|
n: [topLeft, topRight],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import type {
|
|||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ARROW_TYPE } from "../constants";
|
import { ARROW_TYPE } from "../constants";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ describe("elbow arrow routing", () => {
|
|||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||||
point(-45 - arrow.x, -100.1 - arrow.y),
|
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
point(45 - arrow.x, 99.9 - arrow.y),
|
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
||||||
]);
|
]);
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
@@ -69,7 +69,7 @@ describe("elbow arrow routing", () => {
|
|||||||
y: -100.1,
|
y: -100.1,
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 200,
|
height: 200,
|
||||||
points: [point(0, 0), point(90, 200)],
|
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(rectangle1);
|
scene.insertElement(rectangle1);
|
||||||
scene.insertElement(rectangle2);
|
scene.insertElement(rectangle2);
|
||||||
@@ -81,7 +81,7 @@ describe("elbow arrow routing", () => {
|
|||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]);
|
mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]);
|
||||||
|
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
pointScaleFromOrigin,
|
pointScaleFromOrigin,
|
||||||
pointTranslate,
|
pointTranslate,
|
||||||
vector,
|
vector,
|
||||||
@@ -743,13 +743,13 @@ const getDonglePosition = (
|
|||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
switch (heading) {
|
switch (heading) {
|
||||||
case HEADING_UP:
|
case HEADING_UP:
|
||||||
return point(p[0], bounds[1]);
|
return pointFrom(p[0], bounds[1]);
|
||||||
case HEADING_RIGHT:
|
case HEADING_RIGHT:
|
||||||
return point(bounds[2], p[1]);
|
return pointFrom(bounds[2], p[1]);
|
||||||
case HEADING_DOWN:
|
case HEADING_DOWN:
|
||||||
return point(p[0], bounds[3]);
|
return pointFrom(p[0], bounds[3]);
|
||||||
}
|
}
|
||||||
return point(bounds[0], p[1]);
|
return pointFrom(bounds[0], p[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const estimateSegmentCount = (
|
const estimateSegmentCount = (
|
||||||
|
|||||||
@@ -0,0 +1,541 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "../types";
|
||||||
|
import { getNonDeletedElements } from "../";
|
||||||
|
import { getSelectedElements } from "../../scene";
|
||||||
|
import type { AppState, ExcalidrawImperativeAPI, ToolType } from "../../types";
|
||||||
|
import type { LangLdr } from "../../i18n";
|
||||||
|
import { registerCustomLangData } from "../../i18n";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Action,
|
||||||
|
ActionName,
|
||||||
|
ActionPredicateFn,
|
||||||
|
CustomActionName,
|
||||||
|
} from "../../actions/types";
|
||||||
|
import { makeCustomActionName } from "../../actions/types";
|
||||||
|
import { registerCustomShortcuts } from "../../actions/shortcuts";
|
||||||
|
import { register } from "../../actions/register";
|
||||||
|
import { hasBoundTextElement, isTextElement } from "../typeChecks";
|
||||||
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
getContainerElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
} from "../textElement";
|
||||||
|
import { ShapeCache } from "../../scene/ShapeCache";
|
||||||
|
import Scene from "../../scene/Scene";
|
||||||
|
|
||||||
|
// Use "let" instead of "const" so we can dynamically add subtypes
|
||||||
|
let subtypeNames: readonly Subtype[] = [];
|
||||||
|
let parentTypeMap: readonly {
|
||||||
|
subtype: Subtype;
|
||||||
|
parentType: ExcalidrawElement["type"];
|
||||||
|
}[] = [];
|
||||||
|
let subtypeActionMap: readonly {
|
||||||
|
subtype: Subtype;
|
||||||
|
actions: readonly ActionName[];
|
||||||
|
}[] = [];
|
||||||
|
let disabledActionMap: readonly {
|
||||||
|
subtype: Subtype;
|
||||||
|
actions: readonly DisabledActionName[];
|
||||||
|
}[] = [];
|
||||||
|
let alwaysEnabledMap: readonly {
|
||||||
|
subtype: Subtype;
|
||||||
|
actions: readonly SubtypeActionName[];
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
export type SubtypeRecord = Readonly<{
|
||||||
|
subtype: Subtype;
|
||||||
|
parents: readonly (ExcalidrawElement["type"] & ToolType)[];
|
||||||
|
actionNames?: readonly SubtypeActionName[];
|
||||||
|
disabledNames?: readonly DisabledActionName[];
|
||||||
|
shortcutMap?: Record<string, string[]>;
|
||||||
|
alwaysEnabledNames?: readonly SubtypeActionName[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Subtype Names
|
||||||
|
export type Subtype = Required<ExcalidrawElement>["subtype"];
|
||||||
|
export const getSubtypeNames = (): readonly Subtype[] => {
|
||||||
|
return subtypeNames;
|
||||||
|
};
|
||||||
|
export const isValidSubtype = (s: any, t: any): s is Subtype =>
|
||||||
|
parentTypeMap.find(
|
||||||
|
(val) => (val.subtype as any) === s && (val.parentType as any) === t,
|
||||||
|
) !== undefined;
|
||||||
|
const isSubtypeName = (s: any): s is Subtype => subtypeNames.includes(s);
|
||||||
|
|
||||||
|
// Subtype Actions
|
||||||
|
|
||||||
|
// Used for context menus in the shape chooser
|
||||||
|
export const hasAlwaysEnabledActions = (s: any): boolean => {
|
||||||
|
if (!isSubtypeName(s)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return alwaysEnabledMap.some((value) => value.subtype === s);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubtypeActionName = string;
|
||||||
|
|
||||||
|
const isSubtypeActionName = (s: any): s is SubtypeActionName =>
|
||||||
|
subtypeActionMap.some((val) => val.actions.includes(s));
|
||||||
|
|
||||||
|
const addSubtypeAction = (action: Action) => {
|
||||||
|
if (isSubtypeActionName(action.name) || isSubtypeName(action.name)) {
|
||||||
|
register(action);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Standard actions disabled by subtypes
|
||||||
|
type DisabledActionName = ActionName;
|
||||||
|
|
||||||
|
const isDisabledActionName = (s: any): s is DisabledActionName =>
|
||||||
|
disabledActionMap.some((val) => val.actions.includes(s));
|
||||||
|
|
||||||
|
// Is the `actionName` one of the subtype actions for `subtype`
|
||||||
|
// (if `isAdded` is true) or one of the standard actions disabled
|
||||||
|
// by `subtype` (if `isAdded` is false)?
|
||||||
|
const isForSubtype = (
|
||||||
|
subtype: ExcalidrawElement["subtype"],
|
||||||
|
actionName: ActionName,
|
||||||
|
isAdded: boolean,
|
||||||
|
) => {
|
||||||
|
const actions = isAdded ? subtypeActionMap : disabledActionMap;
|
||||||
|
const map = actions.find((value) => value.subtype === subtype);
|
||||||
|
if (map) {
|
||||||
|
return map.actions.includes(actionName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSubtypeAction: ActionPredicateFn = function (action) {
|
||||||
|
return isSubtypeActionName(action.name) && !isSubtypeName(action.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subtypeActionPredicate: ActionPredicateFn = function (
|
||||||
|
action,
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
|
) {
|
||||||
|
// We always enable subtype actions. Also let through standard actions
|
||||||
|
// which no subtypes might have disabled.
|
||||||
|
if (
|
||||||
|
isSubtypeName(action.name) ||
|
||||||
|
(!isSubtypeActionName(action.name) && !isDisabledActionName(action.name))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const chosen = appState.editingTextElement
|
||||||
|
? [appState.editingTextElement, ...selectedElements]
|
||||||
|
: selectedElements;
|
||||||
|
// Now handle actions added by subtypes
|
||||||
|
if (isSubtypeActionName(action.name)) {
|
||||||
|
// Has any ExcalidrawElement enabled this actionName through having
|
||||||
|
// its subtype?
|
||||||
|
return (
|
||||||
|
chosen.some((el) => {
|
||||||
|
const e = hasBoundTextElement(el)
|
||||||
|
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
|
||||||
|
: el;
|
||||||
|
return isForSubtype(e.subtype, action.name, true);
|
||||||
|
}) ||
|
||||||
|
// Or has any active subtype enabled this actionName?
|
||||||
|
(appState.activeSubtypes !== undefined &&
|
||||||
|
appState.activeSubtypes?.some((subtype) => {
|
||||||
|
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isForSubtype(subtype, action.name, true);
|
||||||
|
})) ||
|
||||||
|
alwaysEnabledMap.some((value) => {
|
||||||
|
return value.actions.includes(action.name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Now handle standard actions disabled by subtypes
|
||||||
|
if (isDisabledActionName(action.name)) {
|
||||||
|
return (
|
||||||
|
// Has every ExcalidrawElement not disabled this actionName?
|
||||||
|
(chosen.every((el) => {
|
||||||
|
const e = hasBoundTextElement(el)
|
||||||
|
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
|
||||||
|
: el;
|
||||||
|
return !isForSubtype(e.subtype, action.name, false);
|
||||||
|
}) &&
|
||||||
|
// And has every active subtype not disabled this actionName?
|
||||||
|
(appState.activeSubtypes === undefined ||
|
||||||
|
appState.activeSubtypes?.every((subtype) => {
|
||||||
|
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !isForSubtype(subtype, action.name, false);
|
||||||
|
}))) ||
|
||||||
|
// Or can we find an ExcalidrawElement without a valid subtype
|
||||||
|
// which would disable this action if it had a valid subtype?
|
||||||
|
chosen.some((el) => {
|
||||||
|
const e = hasBoundTextElement(el)
|
||||||
|
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
|
||||||
|
: el;
|
||||||
|
return parentTypeMap.some(
|
||||||
|
(value) =>
|
||||||
|
value.parentType === e.type &&
|
||||||
|
!isValidSubtype(e.subtype, e.type) &&
|
||||||
|
isForSubtype(value.subtype, action.name, false),
|
||||||
|
);
|
||||||
|
}) ||
|
||||||
|
chosen.some((el) => {
|
||||||
|
const e = hasBoundTextElement(el)
|
||||||
|
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
|
||||||
|
: el;
|
||||||
|
return (
|
||||||
|
// Would the subtype of e by inself disable this action?
|
||||||
|
isForSubtype(e.subtype, action.name, false) &&
|
||||||
|
// Can we find an ExcalidrawElement which could have the same subtype
|
||||||
|
// as e but whose subtype does not disable this action?
|
||||||
|
chosen.some((el) => {
|
||||||
|
const e2 = hasBoundTextElement(el)
|
||||||
|
? getBoundTextElement(
|
||||||
|
el,
|
||||||
|
app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
)!
|
||||||
|
: el;
|
||||||
|
return (
|
||||||
|
// Does e have a valid subtype whose parent types include the
|
||||||
|
// type of e2, and does the subtype of e2 not disable this action?
|
||||||
|
parentTypeMap
|
||||||
|
.filter((val) => val.subtype === e.subtype)
|
||||||
|
.some((val) => val.parentType === e2.type) &&
|
||||||
|
!isForSubtype(e2.subtype, action.name, false)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Shouldn't happen
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Are any of the parent types of `subtype` shared by any subtype
|
||||||
|
// in the array?
|
||||||
|
export const subtypeCollides = (subtype: Subtype, subtypeArray: Subtype[]) => {
|
||||||
|
const subtypeParents = parentTypeMap
|
||||||
|
.filter((value) => value.subtype === subtype)
|
||||||
|
.map((value) => value.parentType);
|
||||||
|
const subtypeArrayParents = subtypeArray.flatMap((s) =>
|
||||||
|
parentTypeMap
|
||||||
|
.filter((value) => value.subtype === s)
|
||||||
|
.map((value) => value.parentType),
|
||||||
|
);
|
||||||
|
return subtypeParents.some((t) => subtypeArrayParents.includes(t));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subtype Methods
|
||||||
|
export type SubtypeMethods = {
|
||||||
|
clean: (
|
||||||
|
updates: Omit<
|
||||||
|
Partial<ExcalidrawElement>,
|
||||||
|
"id" | "version" | "versionNonce"
|
||||||
|
>,
|
||||||
|
) => Omit<Partial<ExcalidrawElement>, "id" | "version" | "versionNonce">;
|
||||||
|
getEditorStyle: (element: ExcalidrawTextElement) => Record<string, any>;
|
||||||
|
ensureLoaded: (callback?: () => void) => Promise<void>;
|
||||||
|
measureText: (
|
||||||
|
element: Pick<
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "text"
|
||||||
|
| "lineHeight"
|
||||||
|
>,
|
||||||
|
next?: {
|
||||||
|
fontSize?: number;
|
||||||
|
text?: string;
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
},
|
||||||
|
) => { width: number; height: number };
|
||||||
|
render: (
|
||||||
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
) => void;
|
||||||
|
renderSvg: (
|
||||||
|
svgRoot: SVGElement,
|
||||||
|
addToRoot: (node: SVGElement, element: ExcalidrawElement) => void,
|
||||||
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
opt?: { offsetX?: number; offsetY?: number },
|
||||||
|
) => void;
|
||||||
|
wrapText: (
|
||||||
|
element: Pick<
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
| "subtype"
|
||||||
|
| "customData"
|
||||||
|
| "fontSize"
|
||||||
|
| "fontFamily"
|
||||||
|
| "originalText"
|
||||||
|
| "lineHeight"
|
||||||
|
>,
|
||||||
|
containerWidth: number,
|
||||||
|
next?: {
|
||||||
|
fontSize?: number;
|
||||||
|
text?: string;
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MethodMap = { subtype: Subtype; methods: Partial<SubtypeMethods> };
|
||||||
|
const methodMaps = [] as Array<MethodMap>;
|
||||||
|
|
||||||
|
// Use `getSubtypeMethods` to call subtype-specialized methods, like `render`.
|
||||||
|
export const getSubtypeMethods = (
|
||||||
|
subtype: Subtype | undefined,
|
||||||
|
): Partial<SubtypeMethods> | undefined => {
|
||||||
|
const map = methodMaps.find((method) => method.subtype === subtype);
|
||||||
|
return map?.methods;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addSubtypeMethods = (
|
||||||
|
subtype: Subtype,
|
||||||
|
methods: Partial<SubtypeMethods>,
|
||||||
|
) => {
|
||||||
|
if (!methodMaps.find((method) => method.subtype === subtype)) {
|
||||||
|
methodMaps.push({ subtype, methods });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// For a given `ExcalidrawElement` type, return the active subtype
|
||||||
|
// and associated customData (if any) from the AppState. Assume
|
||||||
|
// only one subtype is active for a given `ExcalidrawElement` type
|
||||||
|
// at any given time.
|
||||||
|
export const selectSubtype = (
|
||||||
|
appState: {
|
||||||
|
activeSubtypes?: AppState["activeSubtypes"];
|
||||||
|
customData?: AppState["customData"];
|
||||||
|
},
|
||||||
|
type: ExcalidrawElement["type"],
|
||||||
|
): {
|
||||||
|
subtype?: ExcalidrawElement["subtype"];
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
} => {
|
||||||
|
if (appState.activeSubtypes === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const subtype = appState.activeSubtypes.find((subtype) =>
|
||||||
|
isValidSubtype(subtype, type),
|
||||||
|
);
|
||||||
|
if (subtype === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (appState.customData === undefined || !(subtype in appState.customData)) {
|
||||||
|
return { subtype };
|
||||||
|
}
|
||||||
|
const customData = appState.customData[subtype];
|
||||||
|
return { subtype, customData };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback to re-render subtyped `ExcalidrawElement`s after completing
|
||||||
|
// async loading of the subtype.
|
||||||
|
export type SubtypeLoadedCb = (hasSubtype: SubtypeCheckFn) => void;
|
||||||
|
export type SubtypeCheckFn = (element: ExcalidrawElement) => boolean;
|
||||||
|
|
||||||
|
// Functions to prepare subtypes for use
|
||||||
|
export type SubtypePrepFn = (
|
||||||
|
addSubtypeAction: (action: Action) => void,
|
||||||
|
addLangData: (fallbackLangData: {}, setLanguageAux: LangLdr) => void,
|
||||||
|
onSubtypeLoaded?: SubtypeLoadedCb,
|
||||||
|
) => {
|
||||||
|
actions: Action[];
|
||||||
|
methods: Partial<SubtypeMethods>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is the main method to set up the subtype. The optional
|
||||||
|
// `onSubtypeLoaded` callback may be used to re-render subtyped
|
||||||
|
// `ExcalidrawElement`s after the subtype has finished async loading.
|
||||||
|
// See the MathJax extension in `@excalidraw/extensions` for example.
|
||||||
|
export const prepareSubtype = (
|
||||||
|
record: SubtypeRecord,
|
||||||
|
subtypePrepFn: SubtypePrepFn,
|
||||||
|
onSubtypeLoaded?: SubtypeLoadedCb,
|
||||||
|
): { actions: readonly Action[] | null; methods: Partial<SubtypeMethods> } => {
|
||||||
|
const map = getSubtypeMethods(record.subtype);
|
||||||
|
if (map) {
|
||||||
|
return { actions: null, methods: map };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for undefined/null subtypes and parentTypes
|
||||||
|
if (
|
||||||
|
record.subtype === undefined ||
|
||||||
|
record.subtype === "" ||
|
||||||
|
record.parents === undefined ||
|
||||||
|
record.parents.length === 0
|
||||||
|
) {
|
||||||
|
return { actions: null, methods: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the types
|
||||||
|
const subtype = record.subtype;
|
||||||
|
subtypeNames = [...subtypeNames, subtype];
|
||||||
|
record.parents.forEach((parentType) => {
|
||||||
|
parentTypeMap = [...parentTypeMap, { subtype, parentType }];
|
||||||
|
});
|
||||||
|
if (record.actionNames) {
|
||||||
|
subtypeActionMap = [
|
||||||
|
...subtypeActionMap,
|
||||||
|
{
|
||||||
|
subtype,
|
||||||
|
actions: record.actionNames.map((actionName) =>
|
||||||
|
makeCustomActionName(actionName),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (record.disabledNames) {
|
||||||
|
disabledActionMap = [
|
||||||
|
...disabledActionMap,
|
||||||
|
{ subtype, actions: record.disabledNames },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (record.alwaysEnabledNames) {
|
||||||
|
alwaysEnabledMap = [
|
||||||
|
...alwaysEnabledMap,
|
||||||
|
{
|
||||||
|
subtype,
|
||||||
|
actions: record.alwaysEnabledNames.map((actionName) =>
|
||||||
|
makeCustomActionName(actionName),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const customShortcutMap = record.shortcutMap;
|
||||||
|
if (customShortcutMap) {
|
||||||
|
const shortcutMap: Record<CustomActionName, string[]> = {};
|
||||||
|
for (const key in customShortcutMap) {
|
||||||
|
shortcutMap[makeCustomActionName(key)] = customShortcutMap[key];
|
||||||
|
}
|
||||||
|
registerCustomShortcuts(shortcutMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the subtype
|
||||||
|
const { actions, methods } = subtypePrepFn(
|
||||||
|
addSubtypeAction,
|
||||||
|
registerCustomLangData,
|
||||||
|
onSubtypeLoaded,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the subtype's methods
|
||||||
|
addSubtypeMethods(record.subtype, methods);
|
||||||
|
return { actions, methods };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure all subtypes are loaded before continuing, eg to
|
||||||
|
// render SVG previews of new charts. Chart-relevant subtypes
|
||||||
|
// include math equations in titles or non hand-drawn line styles.
|
||||||
|
export const ensureSubtypesLoadedForElements = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
callback?: () => void,
|
||||||
|
) => {
|
||||||
|
// Only ensure the loading of subtypes which are actually needed.
|
||||||
|
// We don't want to be held up by eg downloading the MathJax SVG fonts
|
||||||
|
// if we don't actually need them yet.
|
||||||
|
const subtypesUsed = [] as Subtype[];
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (
|
||||||
|
"subtype" in el &&
|
||||||
|
isValidSubtype(el.subtype, el.type) &&
|
||||||
|
!subtypesUsed.includes(el.subtype)
|
||||||
|
) {
|
||||||
|
subtypesUsed.push(el.subtype);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await ensureSubtypesLoaded(subtypesUsed, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensureSubtypesLoaded = async (
|
||||||
|
subtypes: Subtype[],
|
||||||
|
callback?: () => void,
|
||||||
|
) => {
|
||||||
|
// Use a for loop so we can do `await map.ensureLoaded()`
|
||||||
|
for (let i = 0; i < subtypes.length; i++) {
|
||||||
|
const subtype = subtypes[i];
|
||||||
|
// Should be defined if prepareSubtype() has run
|
||||||
|
const map = getSubtypeMethods(subtype);
|
||||||
|
if (map?.ensureLoaded) {
|
||||||
|
await map.ensureLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call this method after finishing any async loading for
|
||||||
|
// subtypes of ExcalidrawElement if the newly loaded code
|
||||||
|
// would change the rendering.
|
||||||
|
export const checkRefreshOnSubtypeLoad = (
|
||||||
|
hasSubtype: SubtypeCheckFn,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const elementsMap = new Map() as ElementsMap;
|
||||||
|
for (const element of elements) {
|
||||||
|
if (!element.isDeleted) {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let refreshNeeded = false;
|
||||||
|
const scenes: Scene[] = [];
|
||||||
|
getNonDeletedElements(elements).forEach((element) => {
|
||||||
|
// If the element is of the subtype that was just
|
||||||
|
// registered, update the element's dimensions, mark the
|
||||||
|
// element for a re-render, and indicate the scene needs a refresh.
|
||||||
|
if (hasSubtype(element)) {
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
element,
|
||||||
|
getContainerElement(element, elementsMap),
|
||||||
|
elementsMap,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
refreshNeeded = true;
|
||||||
|
const scene = Scene.getScene(element);
|
||||||
|
if (scene && !scenes.includes(scene)) {
|
||||||
|
// Store in case we have multiple scenes
|
||||||
|
scenes.push(scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Only inform each scene once
|
||||||
|
scenes.forEach((scene) => scene.triggerUpdate());
|
||||||
|
return refreshNeeded;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSubtype = (
|
||||||
|
api: ExcalidrawImperativeAPI | null,
|
||||||
|
record: SubtypeRecord,
|
||||||
|
subtypePrepFn: SubtypePrepFn,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (api) {
|
||||||
|
const prep = api.addSubtype(record, subtypePrepFn);
|
||||||
|
if (prep) {
|
||||||
|
addSubtypeMethods(record.subtype, prep.methods);
|
||||||
|
if (prep.actions) {
|
||||||
|
prep.actions.forEach((action) => api.registerAction(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [api, record, subtypePrepFn]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Theme } from "../../../element/types";
|
||||||
|
import { createIcon, iconFillColor } from "../../../components/icons";
|
||||||
|
|
||||||
|
// We inline font-awesome icons in order to save on js size rather than including the font awesome react library
|
||||||
|
export const mathSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
// fa-square-root-variable-solid
|
||||||
|
d="M289 24.2C292.5 10 305.3 0 320 0H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H345L239 487.8c-3.2 13-14.2 22.6-27.6 24s-26.1-5.5-32.1-17.5L76.2 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H96c12.1 0 23.2 6.8 28.6 17.7l73.3 146.6L289 24.2zM393.4 233.4c12.5-12.5 32.8-12.5 45.3 0L480 274.7l41.4-41.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L525.3 320l41.4 41.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L480 365.3l-41.4 41.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L434.7 320l-41.4-41.4c-12.5-12.5-12.5-32.8 0-45.3z"
|
||||||
|
/>,
|
||||||
|
{ width: 576, height: 512, mirror: true, strokeWidth: 1.25 },
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ExcalidrawImperativeAPI } from "../../../types";
|
||||||
|
import { useSubtype } from "../";
|
||||||
|
import { getMathSubtypeRecord } from "./types";
|
||||||
|
import { prepareMathSubtype } from "./implementation";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
module SREfeature {
|
||||||
|
function custom(locale: string): Promise<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main hook to use the MathJax subtype
|
||||||
|
export const useMathSubtype = (api: ExcalidrawImperativeAPI | null) => {
|
||||||
|
useSubtype(api, getMathSubtypeRecord(), prepareMathSubtype);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"changeMathOnly": "Math display",
|
||||||
|
"mathOnlyTrue": "Math only",
|
||||||
|
"mathOnlyFalse": "Mixed text",
|
||||||
|
"resetUseTex": "Reset math input type",
|
||||||
|
"useTexTrueActive": "✔ Standard input",
|
||||||
|
"useTexTrueInactive": "Standard input",
|
||||||
|
"useTexFalseActive": "✔ Simplified input",
|
||||||
|
"useTexFalseInactive": "Simplified input"
|
||||||
|
},
|
||||||
|
"toolBar": {
|
||||||
|
"math": "Math"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
import { render } from "../../../../tests/test-utils";
|
||||||
|
import { API } from "../../../../tests/helpers/api";
|
||||||
|
import { Excalidraw } from "../../../../index";
|
||||||
|
|
||||||
|
import { measureTextElement } from "../../../textElement";
|
||||||
|
import { ensureSubtypesLoaded } from "../../";
|
||||||
|
import { getMathSubtypeRecord } from "../types";
|
||||||
|
import { prepareMathSubtype } from "../implementation";
|
||||||
|
|
||||||
|
describe("mathjax loaded", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
API.addSubtype(getMathSubtypeRecord(), prepareMathSubtype);
|
||||||
|
await ensureSubtypesLoaded(["math"]);
|
||||||
|
});
|
||||||
|
it("text-only measurements match", async () => {
|
||||||
|
const text = "A quick brown fox jumps over the lazy dog.";
|
||||||
|
const elements = [
|
||||||
|
API.createElement({ type: "text", id: "A", text, subtype: "math" }),
|
||||||
|
API.createElement({ type: "text", id: "B", text }),
|
||||||
|
];
|
||||||
|
const metrics1 = measureTextElement(elements[0]);
|
||||||
|
const metrics2 = measureTextElement(elements[1]);
|
||||||
|
expect(metrics1).toStrictEqual(metrics2);
|
||||||
|
});
|
||||||
|
it("minimum height remains", async () => {
|
||||||
|
const elements = [
|
||||||
|
API.createElement({ type: "text", id: "A", text: "a" }),
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "B",
|
||||||
|
text: "\\(\\alpha\\)",
|
||||||
|
subtype: "math",
|
||||||
|
customData: { useTex: true },
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "C",
|
||||||
|
text: "`beta`",
|
||||||
|
subtype: "math",
|
||||||
|
customData: { useTex: false },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const height = measureTextElement(elements[0]).height;
|
||||||
|
const height1 = measureTextElement(elements[1]).height;
|
||||||
|
const height2 = measureTextElement(elements[2]).height;
|
||||||
|
expect(height).toEqual(height1);
|
||||||
|
expect(height).toEqual(height2);
|
||||||
|
});
|
||||||
|
it("converts math to svgs", async () => {
|
||||||
|
const svgDim = 42;
|
||||||
|
vi.spyOn(SVGElement.prototype, "getBoundingClientRect").mockImplementation(
|
||||||
|
() => new DOMRect(0, 0, svgDim, svgDim),
|
||||||
|
);
|
||||||
|
const elements = [];
|
||||||
|
const type = "text";
|
||||||
|
const subtype = "math";
|
||||||
|
let text = "Math ";
|
||||||
|
elements.push(API.createElement({ type, text }));
|
||||||
|
text = "Math \\(\\alpha\\)";
|
||||||
|
elements.push(
|
||||||
|
API.createElement({ type, subtype, text, customData: { useTex: true } }),
|
||||||
|
);
|
||||||
|
text = "Math `beta`";
|
||||||
|
elements.push(
|
||||||
|
API.createElement({ type, subtype, text, customData: { useTex: false } }),
|
||||||
|
);
|
||||||
|
const metrics = {
|
||||||
|
width: measureTextElement(elements[0]).width + svgDim,
|
||||||
|
height: svgDim,
|
||||||
|
};
|
||||||
|
expect(measureTextElement(elements[1])).toStrictEqual(metrics);
|
||||||
|
expect(measureTextElement(elements[2])).toStrictEqual(metrics);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { getShortcutKey } from "../../../utils";
|
||||||
|
import type { SubtypeRecord } from "../";
|
||||||
|
|
||||||
|
// Exports
|
||||||
|
export const getMathSubtypeRecord = () => mathSubtype;
|
||||||
|
|
||||||
|
// Use `getMathSubtype` so we don't have to export this
|
||||||
|
const mathSubtype: SubtypeRecord = {
|
||||||
|
subtype: "math",
|
||||||
|
parents: ["text"],
|
||||||
|
actionNames: ["useTexTrue", "useTexFalse", "resetUseTex", "changeMathOnly"],
|
||||||
|
disabledNames: ["changeFontFamily"],
|
||||||
|
shortcutMap: {
|
||||||
|
resetUseTex: [getShortcutKey("Shift+R")],
|
||||||
|
},
|
||||||
|
alwaysEnabledNames: ["useTexTrue", "useTexFalse"],
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { SubtypeMethods } from "./subtypes";
|
||||||
|
import { getSubtypeMethods } from "./subtypes";
|
||||||
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
@@ -30,6 +32,30 @@ import {
|
|||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import type { ExtractSetType } from "../utility-types";
|
import type { ExtractSetType } from "../utility-types";
|
||||||
|
|
||||||
|
export const measureTextElement = function (element, next) {
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.measureText) {
|
||||||
|
return map.measureText(element, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
|
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||||
|
const text = next?.text ?? element.text;
|
||||||
|
return measureText(text, font, element.lineHeight);
|
||||||
|
} as SubtypeMethods["measureText"];
|
||||||
|
|
||||||
|
export const wrapTextElement = function (element, containerWidth, next) {
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.wrapText) {
|
||||||
|
return map.wrapText(element, containerWidth, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSize = next?.fontSize ?? element.fontSize;
|
||||||
|
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||||
|
const text = next?.text ?? element.originalText;
|
||||||
|
return wrapText(text, font, containerWidth);
|
||||||
|
} as SubtypeMethods["wrapText"];
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
normalizeEOL(text)
|
normalizeEOL(text)
|
||||||
@@ -64,18 +90,12 @@ export const redrawTextBoundingBox = (
|
|||||||
maxWidth = container
|
maxWidth = container
|
||||||
? getBoundTextMaxWidth(container, textElement)
|
? getBoundTextMaxWidth(container, textElement)
|
||||||
: textElement.width;
|
: textElement.width;
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
||||||
textElement.originalText,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(textElement, {
|
||||||
boundTextUpdates.text,
|
text: boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
});
|
||||||
textElement.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
|
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
|
||||||
if (textElement.autoResize) {
|
if (textElement.autoResize) {
|
||||||
@@ -83,6 +103,14 @@ export const redrawTextBoundingBox = (
|
|||||||
}
|
}
|
||||||
boundTextUpdates.height = metrics.height;
|
boundTextUpdates.height = metrics.height;
|
||||||
|
|
||||||
|
// Maintain coordX for non left-aligned text in case the width has changed
|
||||||
|
if (!container) {
|
||||||
|
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
|
boundTextUpdates.x += textElement.width - metrics.width;
|
||||||
|
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
|
||||||
|
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (container) {
|
if (container) {
|
||||||
const maxContainerHeight = getBoundTextMaxHeight(
|
const maxContainerHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
@@ -191,17 +219,9 @@ export const handleBindTextResize = (
|
|||||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
(transformHandleType !== "n" && transformHandleType !== "s")
|
||||||
) {
|
) {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
text = wrapTextElement(textElement, maxWidth);
|
||||||
textElement.originalText,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
const metrics = measureTextElement(textElement, { text });
|
||||||
text,
|
|
||||||
getFontString(textElement),
|
|
||||||
textElement.lineHeight,
|
|
||||||
);
|
|
||||||
nextHeight = metrics.height;
|
nextHeight = metrics.height;
|
||||||
nextWidth = metrics.width;
|
nextWidth = metrics.width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@@ -42,7 +42,7 @@ describe("textWysiwyg", () => {
|
|||||||
type: "line",
|
type: "line",
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
points: [point(0, 0), point(100, 0)],
|
points: [pointFrom(0, 0), pointFrom(100, 0)],
|
||||||
});
|
});
|
||||||
const textSize = 20;
|
const textSize = 20;
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
@@ -46,12 +47,15 @@ import {
|
|||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
import type { SubtypeMethods } from "./subtypes";
|
||||||
|
import { getSubtypeMethods } from "./subtypes";
|
||||||
import {
|
import {
|
||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
|
offsetX: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
@@ -69,9 +73,18 @@ const getTransform = (
|
|||||||
if (height > maxHeight && zoom.value !== 1) {
|
if (height > maxHeight && zoom.value !== 1) {
|
||||||
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
||||||
}
|
}
|
||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
const offset = offsetX !== 0 ? ` translate(${offsetX}px, 0px)` : "";
|
||||||
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)${offset}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEditorStyle = function (element) {
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.getEditorStyle) {
|
||||||
|
return map.getEditorStyle(element);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
} as SubtypeMethods["getEditorStyle"];
|
||||||
|
|
||||||
export const textWysiwyg = ({
|
export const textWysiwyg = ({
|
||||||
id,
|
id,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -137,14 +150,27 @@ export const textWysiwyg = ({
|
|||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let width = updatedTextElement.width;
|
// Editing metrics
|
||||||
|
const eMetrics = measureText(
|
||||||
|
container && updatedTextElement.containerId
|
||||||
|
? wrapText(
|
||||||
|
updatedTextElement.originalText,
|
||||||
|
getFontString(updatedTextElement),
|
||||||
|
getBoundTextMaxWidth(container, updatedTextElement),
|
||||||
|
)
|
||||||
|
: updatedTextElement.originalText,
|
||||||
|
getFontString(updatedTextElement),
|
||||||
|
updatedTextElement.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
// set to element height by default since that's
|
let width = Math.max(updatedTextElement.width, eMetrics.width);
|
||||||
|
|
||||||
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let height = updatedTextElement.height;
|
let height = Math.max(updatedTextElement.height, eMetrics.height);
|
||||||
|
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = width;
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
@@ -240,8 +266,31 @@ export const textWysiwyg = ({
|
|||||||
width += 0.5;
|
width += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
|
||||||
|
const offWidth = container
|
||||||
|
? Math.min(
|
||||||
|
0,
|
||||||
|
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
|
||||||
|
)
|
||||||
|
: Math.min(maxWidth, updatedTextElement.width) -
|
||||||
|
Math.min(maxWidth, eMetrics.width);
|
||||||
|
const offsetX =
|
||||||
|
textAlign === "right"
|
||||||
|
? offWidth
|
||||||
|
: textAlign === "center"
|
||||||
|
? offWidth / 2
|
||||||
|
: 0;
|
||||||
|
let { width: w, height: h } = updatedTextElement;
|
||||||
|
|
||||||
// add 5% buffer otherwise it causes wysiwyg to jump
|
// add 5% buffer otherwise it causes wysiwyg to jump
|
||||||
height *= 1.05;
|
height *= 1.05;
|
||||||
|
h *= 1.05;
|
||||||
|
|
||||||
|
const transformOrigin =
|
||||||
|
updatedTextElement.width !== eMetrics.width ||
|
||||||
|
updatedTextElement.height !== eMetrics.height
|
||||||
|
? { transformOrigin: `${w / 2}px ${h / 2}px` }
|
||||||
|
: {};
|
||||||
|
|
||||||
const font = getFontString(updatedTextElement);
|
const font = getFontString(updatedTextElement);
|
||||||
|
|
||||||
@@ -261,7 +310,9 @@ export const textWysiwyg = ({
|
|||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
left: `${viewportX - padding}px`,
|
left: `${viewportX - padding}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
|
...transformOrigin,
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
|
offsetX,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
getTextElementAngle(updatedTextElement, container),
|
getTextElementAngle(updatedTextElement, container),
|
||||||
@@ -322,6 +373,7 @@ export const textWysiwyg = ({
|
|||||||
whiteSpace,
|
whiteSpace,
|
||||||
overflowWrap: "break-word",
|
overflowWrap: "break-word",
|
||||||
boxSizing: "content-box",
|
boxSizing: "content-box",
|
||||||
|
...getEditorStyle(element),
|
||||||
});
|
});
|
||||||
editable.value = element.originalText;
|
editable.value = element.originalText;
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
isIOS,
|
isIOS,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
import { point, pointRotateRads } from "../../math";
|
import { pointFrom, pointRotateRads } from "../../math";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
@@ -95,8 +95,8 @@ const generateTransformHandle = (
|
|||||||
angle: Radians,
|
angle: Radians,
|
||||||
): TransformHandle => {
|
): TransformHandle => {
|
||||||
const [xx, yy] = pointRotateRads(
|
const [xx, yy] = pointRotateRads(
|
||||||
point(x + width / 2, y + height / 2),
|
pointFrom(x + width / 2, y + height / 2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
return [xx - width / 2, yy - height / 2, width, height];
|
return [xx - width / 2, yy - height / 2, width, height];
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
updated: number;
|
updated: number;
|
||||||
link: string | null;
|
link: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
subtype?: string;
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { getElementLineSegments } from "./element/bounds";
|
|||||||
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
|
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
|
||||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||||
import type { ReadonlySetLike } from "./utility-types";
|
import type { ReadonlySetLike } from "./utility-types";
|
||||||
import { isPointWithinBounds, point } from "../math";
|
import { isPointWithinBounds, pointFrom } from "../math";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
@@ -159,9 +159,9 @@ export const isCursorInFrame = (
|
|||||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||||
|
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(
|
||||||
point(fx1, fy1),
|
pointFrom(fx1, fy1),
|
||||||
point(cursorCoords.x, cursorCoords.y),
|
pointFrom(cursorCoords.x, cursorCoords.y),
|
||||||
point(fx2, fy2),
|
pointFrom(fx2, fy2),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -104,3 +104,5 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax";
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ if (import.meta.env.DEV) {
|
|||||||
let currentLang: Language = defaultLang;
|
let currentLang: Language = defaultLang;
|
||||||
let currentLangData = {};
|
let currentLangData = {};
|
||||||
|
|
||||||
|
let fallbackCustomLangData = {};
|
||||||
|
const langLoaders: LangLdr[] = [];
|
||||||
|
export type LangLdr = (langCode: string) => Promise<{}>;
|
||||||
|
|
||||||
|
export const registerCustomLangData = (fallbackLangData: {}, ldr: LangLdr) => {
|
||||||
|
if (!langLoaders.includes(ldr)) {
|
||||||
|
fallbackCustomLangData = { ...fallbackLangData, ...fallbackCustomLangData };
|
||||||
|
langLoaders.push(ldr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const setLanguage = async (lang: Language) => {
|
export const setLanguage = async (lang: Language) => {
|
||||||
currentLang = lang;
|
currentLang = lang;
|
||||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||||
@@ -101,6 +112,14 @@ export const setLanguage = async (lang: Language) => {
|
|||||||
console.error(`Failed to load language ${lang.code}:`, error.message);
|
console.error(`Failed to load language ${lang.code}:`, error.message);
|
||||||
currentLangData = fallbackLangData;
|
currentLangData = fallbackLangData;
|
||||||
}
|
}
|
||||||
|
const auxData = langLoaders.map((fn) => fn(currentLang.code));
|
||||||
|
while (auxData.length > 0) {
|
||||||
|
try {
|
||||||
|
currentLangData = { ...(await auxData.pop()), ...currentLangData };
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error loading ${lang.code} extra data:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jotaiStore.set(editorLangCodeAtom, lang.code);
|
jotaiStore.set(editorLangCodeAtom, lang.code);
|
||||||
@@ -123,7 +142,9 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path: NestedKeyOf<typeof fallbackLangData>,
|
path:
|
||||||
|
| NestedKeyOf<typeof fallbackLangData>
|
||||||
|
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -138,6 +159,7 @@ export const t = (
|
|||||||
let translation =
|
let translation =
|
||||||
findPartsForData(currentLangData, parts) ||
|
findPartsForData(currentLangData, parts) ||
|
||||||
findPartsForData(fallbackLangData, parts) ||
|
findPartsForData(fallbackLangData, parts) ||
|
||||||
|
findPartsForData(fallbackCustomLangData, parts) ||
|
||||||
fallback;
|
fallback;
|
||||||
if (translation === undefined) {
|
if (translation === undefined) {
|
||||||
const errorMessage = `Can't find translation for ${path}`;
|
const errorMessage = `Can't find translation for ${path}`;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.13.1",
|
"jotai": "1.13.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
|
"mathjax-full": "3.2.2",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
StaticCanvasRenderConfig,
|
StaticCanvasRenderConfig,
|
||||||
RenderableElementsMap,
|
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { distance, getFontString, isRTL } from "../utils";
|
import { distance, getFontString, isRTL } from "../utils";
|
||||||
@@ -37,6 +36,7 @@ import type {
|
|||||||
PendingExcalidrawElements,
|
PendingExcalidrawElements,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { getSubtypeMethods } from "../element/subtypes";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||||
@@ -251,7 +251,14 @@ const generateElementCanvas = (
|
|||||||
context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
@@ -374,11 +381,22 @@ const drawImagePlaceholder = (
|
|||||||
|
|
||||||
const drawElementOnCanvas = (
|
const drawElementOnCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
|
context.globalAlpha =
|
||||||
|
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
||||||
|
element.opacity) /
|
||||||
|
10000;
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.render) {
|
||||||
|
map.render(element, elementsMap, context);
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
@@ -673,7 +691,7 @@ export const renderSelectionElement = (
|
|||||||
|
|
||||||
export const renderElement = (
|
export const renderElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: RenderableElementsMap,
|
elementsMap: ElementsMap,
|
||||||
allElementsMap: NonDeletedSceneElementsMap,
|
allElementsMap: NonDeletedSceneElementsMap,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -742,7 +760,14 @@ export const renderElement = (
|
|||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
@@ -837,6 +862,7 @@ export const renderElement = (
|
|||||||
|
|
||||||
drawElementOnCanvas(
|
drawElementOnCanvas(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
tempRc,
|
tempRc,
|
||||||
tempCanvasContext,
|
tempCanvasContext,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
@@ -880,7 +906,14 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
rc,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { point, type GlobalPoint, type LocalPoint } from "../../math";
|
import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math";
|
||||||
import { THEME } from "../constants";
|
import { THEME } from "../constants";
|
||||||
import type { PointSnapLine, PointerSnapLine } from "../snapping";
|
import type { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
@@ -140,27 +140,31 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine(
|
drawLine(
|
||||||
point(from[0], from[1] - FULL),
|
pointFrom(from[0], from[1] - FULL),
|
||||||
point(from[0], from[1] + FULL),
|
pointFrom(from[0], from[1] + FULL),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
point(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
||||||
point(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
point(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
||||||
point(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context);
|
drawLine(
|
||||||
|
pointFrom(to[0], to[1] - FULL),
|
||||||
|
pointFrom(to[0], to[1] + FULL),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
@@ -170,27 +174,31 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine(
|
drawLine(
|
||||||
point(from[0] - FULL, from[1]),
|
pointFrom(from[0] - FULL, from[1]),
|
||||||
point(from[0] + FULL, from[1]),
|
pointFrom(from[0] + FULL, from[1]),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
point(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
||||||
point(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
point(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
||||||
point(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context);
|
drawLine(
|
||||||
|
pointFrom(to[0] - FULL, to[1]),
|
||||||
|
pointFrom(to[0] + FULL, to[1]),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
|||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
|
import { getSubtypeMethods } from "../element/subtypes";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
|
||||||
@@ -125,6 +126,15 @@ const renderElementToSvg = (
|
|||||||
root.appendChild(node);
|
root.appendChild(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const map = getSubtypeMethods(element.subtype);
|
||||||
|
if (map?.renderSvg) {
|
||||||
|
map.renderSvg(svgRoot, addToRoot, element, elementsMap, {
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const opacity =
|
const opacity =
|
||||||
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
||||||
element.opacity) /
|
element.opacity) /
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import type { EmbedsValidationStatus } from "../types";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
@@ -408,7 +408,7 @@ export const _generateElementShape = (
|
|||||||
// initial position to it
|
// initial position to it
|
||||||
const points = element.points.length
|
const points = element.points.length
|
||||||
? element.points
|
? element.points
|
||||||
: [point<LocalPoint>(0, 0)];
|
: [pointFrom<LocalPoint>(0, 0)];
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
shape = [
|
shape = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
point,
|
pointFrom,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromPair,
|
pointFromPair,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
@@ -167,15 +167,15 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
? getClosedCurveShape<Point>(
|
? getClosedCurveShape<Point>(
|
||||||
element,
|
element,
|
||||||
roughShape,
|
roughShape,
|
||||||
point<Point>(element.x, element.y),
|
pointFrom<Point>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
)
|
)
|
||||||
: getCurveShape<Point>(
|
: getCurveShape<Point>(
|
||||||
roughShape,
|
roughShape,
|
||||||
point<Point>(element.x, element.y),
|
pointFrom<Point>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
return getFreedrawShape(
|
return getFreedrawShape(
|
||||||
element,
|
element,
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
shouldTestInside(element),
|
shouldTestInside(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(shape[0]);
|
||||||
let currentP = point<P>(0, 0);
|
let currentP = pointFrom<P>(0, 0);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
let controlPoints: P[] | null = null;
|
let controlPoints: P[] | null = null;
|
||||||
@@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = <
|
|||||||
}
|
}
|
||||||
if (op === "bcurveTo") {
|
if (op === "bcurveTo") {
|
||||||
const p0 = currentP;
|
const p0 = currentP;
|
||||||
const p1 = point<P>(data[0], data[1]);
|
const p1 = pointFrom<P>(data[0], data[1]);
|
||||||
const p2 = point<P>(data[2], data[3]);
|
const p2 = pointFrom<P>(data[2], data[3]);
|
||||||
const p3 = point<P>(data[4], data[5]);
|
const p3 = pointFrom<P>(data[4], data[5]);
|
||||||
const distance = pointDistance(p3, endPoint);
|
const distance = pointDistance(p3, endPoint);
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
@@ -279,7 +279,7 @@ export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
|
|||||||
p0[idx] * Math.pow(t, 3);
|
p0[idx] * Math.pow(t, 3);
|
||||||
const tx = equation(t, 0);
|
const tx = equation(t, 0);
|
||||||
const ty = equation(t, 1);
|
const ty = equation(t, 1);
|
||||||
return point(tx, ty);
|
return pointFrom(tx, ty);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
||||||
@@ -301,12 +301,12 @@ const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
|||||||
controlPoints[3],
|
controlPoints[3],
|
||||||
t,
|
t,
|
||||||
);
|
);
|
||||||
pointsOnCurve.push(point(p[0], p[1]));
|
pointsOnCurve.push(pointFrom(p[0], p[1]));
|
||||||
t -= 0.05;
|
t -= 0.05;
|
||||||
}
|
}
|
||||||
if (pointsOnCurve.length) {
|
if (pointsOnCurve.length) {
|
||||||
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
||||||
pointsOnCurve.push(point(endPoint[0], endPoint[1]));
|
pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pointsOnCurve;
|
return pointsOnCurve;
|
||||||
@@ -393,24 +393,24 @@ export const aabbForElement = (
|
|||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = point(bbox.midX, bbox.midY);
|
const center = pointFrom(bbox.midX, bbox.midY);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
point(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [topRightX, topRightY] = pointRotateRads(
|
const [topRightX, topRightY] = pointRotateRads(
|
||||||
point(bbox.maxX, bbox.minY),
|
pointFrom(bbox.maxX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [bottomRightX, bottomRightY] = pointRotateRads(
|
const [bottomRightX, bottomRightY] = pointRotateRads(
|
||||||
point(bbox.maxX, bbox.maxY),
|
pointFrom(bbox.maxX, bbox.maxY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
||||||
point(bbox.minX, bbox.maxY),
|
pointFrom(bbox.minX, bbox.maxY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -442,14 +442,14 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
|
|||||||
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
||||||
|
|
||||||
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
||||||
pointInsideBounds(point(a[0], a[1]), b) ||
|
pointInsideBounds(pointFrom(a[0], a[1]), b) ||
|
||||||
pointInsideBounds(point(a[2], a[1]), b) ||
|
pointInsideBounds(pointFrom(a[2], a[1]), b) ||
|
||||||
pointInsideBounds(point(a[2], a[3]), b) ||
|
pointInsideBounds(pointFrom(a[2], a[3]), b) ||
|
||||||
pointInsideBounds(point(a[0], a[3]), b) ||
|
pointInsideBounds(pointFrom(a[0], a[3]), b) ||
|
||||||
pointInsideBounds(point(b[0], b[1]), a) ||
|
pointInsideBounds(pointFrom(b[0], b[1]), a) ||
|
||||||
pointInsideBounds(point(b[2], b[1]), a) ||
|
pointInsideBounds(pointFrom(b[2], b[1]), a) ||
|
||||||
pointInsideBounds(point(b[2], b[3]), a) ||
|
pointInsideBounds(pointFrom(b[2], b[3]), a) ||
|
||||||
pointInsideBounds(point(b[0], b[3]), a);
|
pointInsideBounds(pointFrom(b[0], b[3]), a);
|
||||||
|
|
||||||
export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { InclusiveRange } from "../math";
|
import type { InclusiveRange } from "../math";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
rangeIntersection,
|
rangeIntersection,
|
||||||
@@ -228,52 +228,52 @@ export const getElementsCorners = (
|
|||||||
!boundingBoxCorners
|
!boundingBoxCorners
|
||||||
) {
|
) {
|
||||||
const leftMid = pointRotateRads<GlobalPoint>(
|
const leftMid = pointRotateRads<GlobalPoint>(
|
||||||
point(x1, y1 + halfHeight),
|
pointFrom(x1, y1 + halfHeight),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topMid = pointRotateRads<GlobalPoint>(
|
const topMid = pointRotateRads<GlobalPoint>(
|
||||||
point(x1 + halfWidth, y1),
|
pointFrom(x1 + halfWidth, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const rightMid = pointRotateRads<GlobalPoint>(
|
const rightMid = pointRotateRads<GlobalPoint>(
|
||||||
point(x2, y1 + halfHeight),
|
pointFrom(x2, y1 + halfHeight),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomMid = pointRotateRads<GlobalPoint>(
|
const bottomMid = pointRotateRads<GlobalPoint>(
|
||||||
point(x1 + halfWidth, y2),
|
pointFrom(x1 + halfWidth, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const center = point<GlobalPoint>(cx, cy);
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [leftMid, topMid, rightMid, bottomMid]
|
? [leftMid, topMid, rightMid, bottomMid]
|
||||||
: [leftMid, topMid, rightMid, bottomMid, center];
|
: [leftMid, topMid, rightMid, bottomMid, center];
|
||||||
} else {
|
} else {
|
||||||
const topLeft = pointRotateRads<GlobalPoint>(
|
const topLeft = pointRotateRads<GlobalPoint>(
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topRight = pointRotateRads<GlobalPoint>(
|
const topRight = pointRotateRads<GlobalPoint>(
|
||||||
point(x2, y1),
|
pointFrom(x2, y1),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomLeft = pointRotateRads<GlobalPoint>(
|
const bottomLeft = pointRotateRads<GlobalPoint>(
|
||||||
point(x1, y2),
|
pointFrom(x1, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomRight = pointRotateRads<GlobalPoint>(
|
const bottomRight = pointRotateRads<GlobalPoint>(
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
point(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const center = point<GlobalPoint>(cx, cy);
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||||
@@ -287,18 +287,18 @@ export const getElementsCorners = (
|
|||||||
const width = maxX - minX;
|
const width = maxX - minX;
|
||||||
const height = maxY - minY;
|
const height = maxY - minY;
|
||||||
|
|
||||||
const topLeft = point<GlobalPoint>(minX, minY);
|
const topLeft = pointFrom<GlobalPoint>(minX, minY);
|
||||||
const topRight = point<GlobalPoint>(maxX, minY);
|
const topRight = pointFrom<GlobalPoint>(maxX, minY);
|
||||||
const bottomLeft = point<GlobalPoint>(minX, maxY);
|
const bottomLeft = pointFrom<GlobalPoint>(minX, maxY);
|
||||||
const bottomRight = point<GlobalPoint>(maxX, maxY);
|
const bottomRight = pointFrom<GlobalPoint>(maxX, maxY);
|
||||||
const center = point<GlobalPoint>(minX + width / 2, minY + height / 2);
|
const center = pointFrom<GlobalPoint>(minX + width / 2, minY + height / 2);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||||
: [topLeft, topRight, bottomLeft, bottomRight, center];
|
: [topLeft, topRight, bottomLeft, bottomRight, center];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map((p) => point(round(p[0]), round(p[1])));
|
return result.map((p) => pointFrom(round(p[0]), round(p[1])));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReferenceElements = (
|
const getReferenceElements = (
|
||||||
@@ -375,8 +375,11 @@ export const getVisibleGaps = (
|
|||||||
horizontalGaps.push({
|
horizontalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
|
startSide: [
|
||||||
endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
|
pointFrom(startMaxX, startMinY),
|
||||||
|
pointFrom(startMaxX, startMaxY),
|
||||||
|
],
|
||||||
|
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)],
|
||||||
length: endMinX - startMaxX,
|
length: endMinX - startMaxX,
|
||||||
overlap: rangeIntersection(
|
overlap: rangeIntersection(
|
||||||
rangeInclusive(startMinY, startMaxY),
|
rangeInclusive(startMinY, startMaxY),
|
||||||
@@ -415,8 +418,11 @@ export const getVisibleGaps = (
|
|||||||
verticalGaps.push({
|
verticalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
|
startSide: [
|
||||||
endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
|
pointFrom(startMinX, startMaxY),
|
||||||
|
pointFrom(startMaxX, startMaxY),
|
||||||
|
],
|
||||||
|
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)],
|
||||||
length: endMinY - startMaxY,
|
length: endMinY - startMaxY,
|
||||||
overlap: rangeIntersection(
|
overlap: rangeIntersection(
|
||||||
rangeInclusive(startMinX, startMaxX),
|
rangeInclusive(startMinX, startMaxX),
|
||||||
@@ -832,7 +838,7 @@ const createPointSnapLines = (
|
|||||||
}
|
}
|
||||||
snapsX[key].push(
|
snapsX[key].push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
point<GlobalPoint>(round(p[0]), round(p[1])),
|
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -849,7 +855,7 @@ const createPointSnapLines = (
|
|||||||
}
|
}
|
||||||
snapsY[key].push(
|
snapsY[key].push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
point<GlobalPoint>(round(p[0]), round(p[1])),
|
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -863,7 +869,7 @@ const createPointSnapLines = (
|
|||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
return point<GlobalPoint>(Number(key), p[1]);
|
return pointFrom<GlobalPoint>(Number(key), p[1]);
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[1] - b[1]),
|
.sort((a, b) => a[1] - b[1]),
|
||||||
),
|
),
|
||||||
@@ -876,7 +882,7 @@ const createPointSnapLines = (
|
|||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
return point<GlobalPoint>(p[0], Number(key));
|
return pointFrom<GlobalPoint>(p[0], Number(key));
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[0] - b[0]),
|
.sort((a, b) => a[0] - b[0]),
|
||||||
),
|
),
|
||||||
@@ -940,16 +946,16 @@ const createGapSnapLines = (
|
|||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
point(gapSnap.gap.startSide[0][0], gapLineY),
|
pointFrom(gapSnap.gap.startSide[0][0], gapLineY),
|
||||||
point(minX, gapLineY),
|
pointFrom(minX, gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
point(maxX, gapLineY),
|
pointFrom(maxX, gapLineY),
|
||||||
point(gapSnap.gap.endSide[0][0], gapLineY),
|
pointFrom(gapSnap.gap.endSide[0][0], gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -966,16 +972,16 @@ const createGapSnapLines = (
|
|||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
point(gapLineX, gapSnap.gap.startSide[0][1]),
|
pointFrom(gapLineX, gapSnap.gap.startSide[0][1]),
|
||||||
point(gapLineX, minY),
|
pointFrom(gapLineX, minY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
point(gapLineX, maxY),
|
pointFrom(gapLineX, maxY),
|
||||||
point(gapLineX, gapSnap.gap.endSide[0][1]),
|
pointFrom(gapLineX, gapSnap.gap.endSide[0][1]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -991,12 +997,15 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
points: [
|
||||||
|
pointFrom(startMaxX, gapLineY),
|
||||||
|
pointFrom(endMinX, gapLineY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
|
points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1011,12 +1020,18 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
|
points: [
|
||||||
|
pointFrom(maxX, gapLineY),
|
||||||
|
pointFrom(startMinX, gapLineY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
points: [
|
||||||
|
pointFrom(startMaxX, gapLineY),
|
||||||
|
pointFrom(endMinX, gapLineY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1031,12 +1046,18 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
|
points: [
|
||||||
|
pointFrom(gapLineX, maxY),
|
||||||
|
pointFrom(gapLineX, startMinY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
points: [
|
||||||
|
pointFrom(gapLineX, startMaxY),
|
||||||
|
pointFrom(gapLineX, endMinY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1051,12 +1072,15 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
points: [
|
||||||
|
pointFrom(gapLineX, startMaxY),
|
||||||
|
pointFrom(gapLineX, endMinY),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
|
points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1070,7 +1094,7 @@ const createGapSnapLines = (
|
|||||||
return {
|
return {
|
||||||
...gapSnapLine,
|
...gapSnapLine,
|
||||||
points: gapSnapLine.points.map((p) =>
|
points: gapSnapLine.points.map((p) =>
|
||||||
point(round(p[0]), round(p[1])),
|
pointFrom(round(p[0]), round(p[1])),
|
||||||
) as PointPair,
|
) as PointPair,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -1120,35 +1144,35 @@ export const snapResizingElements = (
|
|||||||
if (transformHandle) {
|
if (transformHandle) {
|
||||||
switch (transformHandle) {
|
switch (transformHandle) {
|
||||||
case "e": {
|
case "e": {
|
||||||
selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
|
selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "w": {
|
case "w": {
|
||||||
selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
|
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "n": {
|
case "n": {
|
||||||
selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
|
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "s": {
|
case "s": {
|
||||||
selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
|
selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ne": {
|
case "ne": {
|
||||||
selectionSnapPoints.push(point(maxX, minY));
|
selectionSnapPoints.push(pointFrom(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "nw": {
|
case "nw": {
|
||||||
selectionSnapPoints.push(point(minX, minY));
|
selectionSnapPoints.push(pointFrom(minX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "se": {
|
case "se": {
|
||||||
selectionSnapPoints.push(point(maxX, maxY));
|
selectionSnapPoints.push(pointFrom(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sw": {
|
case "sw": {
|
||||||
selectionSnapPoints.push(point(minX, maxY));
|
selectionSnapPoints.push(pointFrom(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1191,10 +1215,10 @@ export const snapResizingElements = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const corners: GlobalPoint[] = [
|
const corners: GlobalPoint[] = [
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
point(x1, y2),
|
pointFrom(x1, y2),
|
||||||
point(x2, y1),
|
pointFrom(x2, y1),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
];
|
];
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
@@ -1231,7 +1255,7 @@ export const snapNewElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: GlobalPoint[] = [
|
const selectionSnapPoints: GlobalPoint[] = [
|
||||||
point(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
||||||
];
|
];
|
||||||
|
|
||||||
const snapDistance = getSnapDistance(app.state.zoom.value);
|
const snapDistance = getSnapDistance(app.state.zoom.value);
|
||||||
@@ -1331,7 +1355,7 @@ export const getSnapLinesAtPointer = (
|
|||||||
|
|
||||||
verticalSnapLines.push({
|
verticalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, point(corner[0], pointer.y)],
|
points: [corner, pointFrom(corner[0], pointer.y)],
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1347,7 +1371,7 @@ export const getSnapLinesAtPointer = (
|
|||||||
|
|
||||||
horizontalSnapLines.push({
|
horizontalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, point(pointer.x, corner[1])],
|
points: [corner, pointFrom(pointer.x, corner[1])],
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { API } from "./helpers/api";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -32,7 +32,12 @@ describe("element binding", () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 1,
|
height: 1,
|
||||||
points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)],
|
points: [
|
||||||
|
pointFrom(0, 0),
|
||||||
|
pointFrom(0, 0),
|
||||||
|
pointFrom(100, 0),
|
||||||
|
pointFrom(100, 0),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
API.setElements([rect, arrow]);
|
API.setElements([rect, arrow]);
|
||||||
expect(arrow.startBinding).toBe(null);
|
expect(arrow.startBinding).toBe(null);
|
||||||
@@ -310,7 +315,7 @@ describe("element binding", () => {
|
|||||||
const arrow1 = API.createElement({
|
const arrow1 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
points: [point(0, 0), point(0, -87.45777932247563)],
|
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
@@ -328,7 +333,7 @@ describe("element binding", () => {
|
|||||||
const arrow2 = API.createElement({
|
const arrow2 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow2",
|
id: "arrow2",
|
||||||
points: [point(0, 0), point(0, -87.45777932247563)],
|
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "text1",
|
elementId: "text1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
|
import { getShortcutKey } from "../utils";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { render } from "./test-utils";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
import {
|
||||||
|
getShortcutFromShortcutName,
|
||||||
|
registerCustomShortcuts,
|
||||||
|
} from "../actions/shortcuts";
|
||||||
|
import type {
|
||||||
|
Action,
|
||||||
|
ActionPredicateFn,
|
||||||
|
ActionResult,
|
||||||
|
CustomActionName,
|
||||||
|
} from "../actions/types";
|
||||||
|
import { makeCustomActionName } from "../actions/types";
|
||||||
|
import {
|
||||||
|
actionChangeFontFamily,
|
||||||
|
actionChangeFontSize,
|
||||||
|
} from "../actions/actionProperties";
|
||||||
|
import { isTextElement } from "../element";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("regression tests", () => {
|
||||||
|
it("should retrieve custom shortcuts", () => {
|
||||||
|
const shortcutName = makeCustomActionName("test");
|
||||||
|
const shortcuts: Record<CustomActionName, string[]> = {};
|
||||||
|
shortcuts[shortcutName] = [
|
||||||
|
getShortcutKey("CtrlOrCmd+1"),
|
||||||
|
getShortcutKey("CtrlOrCmd+2"),
|
||||||
|
];
|
||||||
|
registerCustomShortcuts(shortcuts);
|
||||||
|
expect(getShortcutFromShortcutName(shortcutName)).toBe("Ctrl+1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply universal action predicates", async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
// Create the test elements
|
||||||
|
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
|
||||||
|
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
|
||||||
|
const el3 = API.createElement({ type: "text", id: "C", y: 60 });
|
||||||
|
const el12: ExcalidrawElement[] = [el1, el2];
|
||||||
|
const el13: ExcalidrawElement[] = [el1, el3];
|
||||||
|
const el23: ExcalidrawElement[] = [el2, el3];
|
||||||
|
const el123: ExcalidrawElement[] = [el1, el2, el3];
|
||||||
|
// Set up the custom Action enablers
|
||||||
|
const enableName = "custom.enable";
|
||||||
|
const enableAction: Action = {
|
||||||
|
name: enableName,
|
||||||
|
label: "",
|
||||||
|
perform: (): ActionResult => {
|
||||||
|
return {} as ActionResult;
|
||||||
|
},
|
||||||
|
trackEvent: false,
|
||||||
|
};
|
||||||
|
const enabler: ActionPredicateFn = function (action, elements) {
|
||||||
|
if (action.name !== enableName || elements.some((el) => el.y === 30)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Set up the standard Action disablers
|
||||||
|
const disabled1 = actionChangeFontFamily;
|
||||||
|
const disabled2 = actionChangeFontSize;
|
||||||
|
const disabler: ActionPredicateFn = function (action, elements) {
|
||||||
|
if (
|
||||||
|
action.name === disabled2.name &&
|
||||||
|
elements.some((el) => el.y === 0 || isTextElement(el))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// Test the custom Action enablers
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
am.registerActionPredicate(enabler);
|
||||||
|
expect(am.isActionEnabled(enableAction, { elements: el12 })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(enableAction, { elements: el13 })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(enableAction, { elements: el23 })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(disabled1, { elements: el12 })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(disabled1, { elements: el13 })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(disabled1, { elements: el23 })).toBe(true);
|
||||||
|
// Test the standard Action disablers
|
||||||
|
am.registerActionPredicate(disabler);
|
||||||
|
expect(am.isActionEnabled(disabled1, { elements: el123 })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: [el1] })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: [el2] })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: [el3] })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: el12 })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: el23 })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(disabled2, { elements: el13 })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,7 +28,7 @@ import { getBoundTextElementPosition } from "../element/textElement";
|
|||||||
import { createPasteEvent } from "../clipboard";
|
import { createPasteEvent } from "../clipboard";
|
||||||
import { arrayToMap, cloneJSON } from "../utils";
|
import { arrayToMap, cloneJSON } from "../utils";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { point, type Radians } from "../../math";
|
import { pointFrom, type Radians } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -146,9 +146,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
|
|||||||
link: null,
|
link: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
points: [
|
points: [
|
||||||
point<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
point<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
||||||
point<LocalPoint>(828.0126953125, 410.51605224609375),
|
pointFrom<LocalPoint>(828.0126953125, 410.51605224609375),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ import fs from "fs";
|
|||||||
import util from "util";
|
import util from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
|
import type {
|
||||||
|
SubtypeLoadedCb,
|
||||||
|
SubtypePrepFn,
|
||||||
|
SubtypeRecord,
|
||||||
|
} from "../../element/subtypes";
|
||||||
import {
|
import {
|
||||||
|
checkRefreshOnSubtypeLoad,
|
||||||
|
prepareSubtype,
|
||||||
|
selectSubtype,
|
||||||
|
} from "../../element/subtypes";
|
||||||
|
import {
|
||||||
|
maybeGetSubtypeProps,
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
newEmbeddableElement,
|
newEmbeddableElement,
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
@@ -38,7 +49,7 @@ import type App from "../../components/App";
|
|||||||
import { createTestHook } from "../../components/App";
|
import { createTestHook } from "../../components/App";
|
||||||
import type { Action } from "../../actions/types";
|
import type { Action } from "../../actions/types";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { point, type LocalPoint, type Radians } from "../../../math";
|
import { pointFrom, type LocalPoint, type Radians } from "../../../math";
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
// so that window.h is available when App.tsx is not imported as well.
|
// so that window.h is available when App.tsx is not imported as well.
|
||||||
@@ -47,6 +58,19 @@ createTestHook();
|
|||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
export class API {
|
export class API {
|
||||||
|
static addSubtype = (record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) => {
|
||||||
|
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||||
|
if (checkRefreshOnSubtypeLoad(hasSubtype, h.elements)) {
|
||||||
|
h.app.refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||||
|
if (prep.actions) {
|
||||||
|
h.app.actionManager.registerAll(prep.actions);
|
||||||
|
}
|
||||||
|
return prep;
|
||||||
|
};
|
||||||
|
|
||||||
static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => {
|
static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => {
|
||||||
act(() => {
|
act(() => {
|
||||||
h.app.updateScene(...args);
|
h.app.updateScene(...args);
|
||||||
@@ -175,6 +199,8 @@ export class API {
|
|||||||
verticalAlign?: T extends "text"
|
verticalAlign?: T extends "text"
|
||||||
? ExcalidrawTextElement["verticalAlign"]
|
? ExcalidrawTextElement["verticalAlign"]
|
||||||
: never;
|
: never;
|
||||||
|
subtype?: ExcalidrawElement["subtype"];
|
||||||
|
customData?: ExcalidrawElement["customData"];
|
||||||
boundElements?: ExcalidrawGenericElement["boundElements"];
|
boundElements?: ExcalidrawGenericElement["boundElements"];
|
||||||
containerId?: T extends "text"
|
containerId?: T extends "text"
|
||||||
? ExcalidrawTextElement["containerId"]
|
? ExcalidrawTextElement["containerId"]
|
||||||
@@ -214,6 +240,14 @@ export class API {
|
|||||||
|
|
||||||
const appState = h?.state || getDefaultAppState();
|
const appState = h?.state || getDefaultAppState();
|
||||||
|
|
||||||
|
const custom = maybeGetSubtypeProps(
|
||||||
|
{
|
||||||
|
subtype: rest.subtype ?? selectSubtype(appState, type)?.subtype,
|
||||||
|
customData:
|
||||||
|
rest.customData ?? selectSubtype(appState, type)?.customData,
|
||||||
|
},
|
||||||
|
type,
|
||||||
|
);
|
||||||
const base: Omit<
|
const base: Omit<
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
| "id"
|
| "id"
|
||||||
@@ -228,6 +262,7 @@ export class API {
|
|||||||
| "link"
|
| "link"
|
||||||
| "updated"
|
| "updated"
|
||||||
> = {
|
> = {
|
||||||
|
...custom,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
frameId: rest.frameId ?? null,
|
frameId: rest.frameId ?? null,
|
||||||
@@ -307,8 +342,8 @@ export class API {
|
|||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
point<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
point<LocalPoint>(100, 100),
|
pointFrom<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
elbowed: rest.elbowed ?? false,
|
elbowed: rest.elbowed ?? false,
|
||||||
});
|
});
|
||||||
@@ -320,8 +355,8 @@ export class API {
|
|||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
point<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
point<LocalPoint>(100, 100),
|
pointFrom<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"toolBar": {
|
||||||
|
"test": "Test",
|
||||||
|
"test2": "Test 2",
|
||||||
|
"test3": "Test 3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ import { getTextEditor } from "../queries/dom";
|
|||||||
import { arrayToMap } from "../../utils";
|
import { arrayToMap } from "../../utils";
|
||||||
import { createTestHook } from "../../components/App";
|
import { createTestHook } from "../../components/App";
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
|
import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { pointFrom, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
// so that window.h is available when App.tsx is not imported as well.
|
// so that window.h is available when App.tsx is not imported as well.
|
||||||
createTestHook();
|
createTestHook();
|
||||||
@@ -142,7 +142,7 @@ const getElementPointForSelection = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const target = point<GlobalPoint>(
|
const target = pointFrom<GlobalPoint>(
|
||||||
x +
|
x +
|
||||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||||
y,
|
y,
|
||||||
@@ -151,9 +151,12 @@ const getElementPointForSelection = (
|
|||||||
|
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element)) {
|
||||||
const bounds = getElementPointsCoords(element, element.points);
|
const bounds = getElementPointsCoords(element, element.points);
|
||||||
center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2);
|
center = pointFrom(
|
||||||
|
(bounds[0] + bounds[2]) / 2,
|
||||||
|
(bounds[1] + bounds[3]) / 2,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
center = point(x + width / 2, y + height / 2);
|
center = pointFrom(x + width / 2, y + height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
@@ -469,8 +472,8 @@ export class UI {
|
|||||||
const width = initialWidth ?? initialHeight ?? size;
|
const width = initialWidth ?? initialHeight ?? size;
|
||||||
const height = initialHeight ?? size;
|
const height = initialHeight ?? size;
|
||||||
const points: LocalPoint[] = initialPoints ?? [
|
const points: LocalPoint[] = initialPoints ?? [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(width, height),
|
pointFrom(width, height),
|
||||||
];
|
];
|
||||||
|
|
||||||
UI.clickTool(type);
|
UI.clickTool(type);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { HistoryEntry } from "../history";
|
|||||||
import { AppStateChange, ElementsChange } from "../change";
|
import { AppStateChange, ElementsChange } from "../change";
|
||||||
import { Snapshot, StoreAction } from "../store";
|
import { Snapshot, StoreAction } from "../store";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -2041,9 +2041,9 @@ describe("history", () => {
|
|||||||
width: 178.9000000000001,
|
width: 178.9000000000001,
|
||||||
height: 236.10000000000002,
|
height: 236.10000000000002,
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(178.9000000000001, 0),
|
pointFrom(178.9000000000001, 0),
|
||||||
point(178.9000000000001, 236.10000000000002),
|
pointFrom(178.9000000000001, 236.10000000000002),
|
||||||
],
|
],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
||||||
@@ -2159,11 +2159,11 @@ describe("history", () => {
|
|||||||
elements: [
|
elements: [
|
||||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(5, 5),
|
pointFrom(5, 5),
|
||||||
point(10, 10),
|
pointFrom(10, 10),
|
||||||
point(15, 15),
|
pointFrom(15, 15),
|
||||||
point(20, 20),
|
pointFrom(20, 20),
|
||||||
] as LocalPoint[],
|
] as LocalPoint[],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { GlobalPoint } from "../../math";
|
import type { GlobalPoint } from "../../math";
|
||||||
import { pointCenter, point } from "../../math";
|
import { pointCenter, pointFrom } from "../../math";
|
||||||
|
|
||||||
const renderInteractiveScene = vi.spyOn(
|
const renderInteractiveScene = vi.spyOn(
|
||||||
InteractiveCanvas,
|
InteractiveCanvas,
|
||||||
@@ -57,8 +57,8 @@ describe("Test Linear Elements", () => {
|
|||||||
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||||
});
|
});
|
||||||
|
|
||||||
const p1 = point<GlobalPoint>(20, 20);
|
const p1 = pointFrom<GlobalPoint>(20, 20);
|
||||||
const p2 = point<GlobalPoint>(60, 20);
|
const p2 = pointFrom<GlobalPoint>(60, 20);
|
||||||
const midpoint = pointCenter<GlobalPoint>(p1, p2);
|
const midpoint = pointCenter<GlobalPoint>(p1, p2);
|
||||||
const delta = 50;
|
const delta = 50;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -75,7 +75,7 @@ describe("Test Linear Elements", () => {
|
|||||||
height: 0,
|
height: 0,
|
||||||
type,
|
type,
|
||||||
roughness,
|
roughness,
|
||||||
points: [point(0, 0), point(p2[0] - p1[0], p2[1] - p1[1])],
|
points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])],
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
@@ -99,9 +99,9 @@ describe("Test Linear Elements", () => {
|
|||||||
type,
|
type,
|
||||||
roughness,
|
roughness,
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(p3[0], p3[1]),
|
pointFrom(p3[0], p3[1]),
|
||||||
point(p2[0] - p1[0], p2[1] - p1[1]),
|
pointFrom(p2[0] - p1[0], p2[1] - p1[1]),
|
||||||
],
|
],
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
@@ -161,7 +161,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
|
drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
|
||||||
|
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(line.x).toBe(originalX);
|
expect(line.x).toBe(originalX);
|
||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
@@ -184,7 +184,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
@@ -248,7 +248,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
|
drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
|
||||||
expect(line.x).toBe(originalX);
|
expect(line.x).toBe(originalX);
|
||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
@@ -261,7 +261,7 @@ describe("Test Linear Elements", () => {
|
|||||||
enterLineEditingMode(line);
|
enterLineEditingMode(line);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
@@ -356,7 +356,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const startPoint = pointCenter(points[0], midPoints[0]!);
|
const startPoint = pointCenter(points[0], midPoints[0]!);
|
||||||
const deltaX = 50;
|
const deltaX = 50;
|
||||||
const deltaY = 20;
|
const deltaY = 20;
|
||||||
const endPoint = point<GlobalPoint>(
|
const endPoint = pointFrom<GlobalPoint>(
|
||||||
startPoint[0] + deltaX,
|
startPoint[0] + deltaX,
|
||||||
startPoint[1] + deltaY,
|
startPoint[1] + deltaY,
|
||||||
);
|
);
|
||||||
@@ -399,8 +399,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint = point<GlobalPoint>(55, 45);
|
const firstSegmentMidpoint = pointFrom<GlobalPoint>(55, 45);
|
||||||
const lastSegmentMidpoint = point<GlobalPoint>(75, 40);
|
const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40);
|
||||||
|
|
||||||
let line: ExcalidrawLinearElement;
|
let line: ExcalidrawLinearElement;
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line via first segment midpoint
|
// drag line via first segment midpoint
|
||||||
drag(
|
drag(
|
||||||
firstSegmentMidpoint,
|
firstSegmentMidpoint,
|
||||||
point(
|
pointFrom(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -426,7 +426,10 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
pointFrom(
|
||||||
|
lastSegmentMidpoint[0] + delta,
|
||||||
|
lastSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
@@ -475,10 +478,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -516,10 +519,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -556,7 +559,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// dragging line from last segment midpoint
|
// dragging line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
||||||
);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
@@ -589,11 +592,11 @@ describe("Test Linear Elements", () => {
|
|||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint = point<GlobalPoint>(
|
const firstSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||||
55.9697848965255,
|
55.9697848965255,
|
||||||
47.442326230998205,
|
47.442326230998205,
|
||||||
);
|
);
|
||||||
const lastSegmentMidpoint = point<GlobalPoint>(
|
const lastSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||||
76.08587175006699,
|
76.08587175006699,
|
||||||
43.294165939653226,
|
43.294165939653226,
|
||||||
);
|
);
|
||||||
@@ -612,7 +615,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from first segment midpoint
|
// drag line from first segment midpoint
|
||||||
drag(
|
drag(
|
||||||
firstSegmentMidpoint,
|
firstSegmentMidpoint,
|
||||||
point(
|
pointFrom(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -622,7 +625,10 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
pointFrom(
|
||||||
|
lastSegmentMidpoint[0] + delta,
|
||||||
|
lastSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
@@ -669,10 +675,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@@ -717,10 +723,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -751,7 +757,10 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
pointFrom(
|
||||||
|
lastSegmentMidpoint[0] + delta,
|
||||||
|
lastSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
@@ -811,8 +820,8 @@ describe("Test Linear Elements", () => {
|
|||||||
API.setSelectedElements([line]);
|
API.setSelectedElements([line]);
|
||||||
enterLineEditingMode(line, true);
|
enterLineEditingMode(line, true);
|
||||||
drag(
|
drag(
|
||||||
point(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
||||||
point(
|
pointFrom(
|
||||||
dragEndPositionOffset[0] + line.x,
|
dragEndPositionOffset[0] + line.x,
|
||||||
dragEndPositionOffset[1] + line.y,
|
dragEndPositionOffset[1] + line.y,
|
||||||
),
|
),
|
||||||
@@ -927,14 +936,14 @@ describe("Test Linear Elements", () => {
|
|||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint = point<GlobalPoint>(
|
const firstSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||||
55.9697848965255,
|
55.9697848965255,
|
||||||
47.442326230998205,
|
47.442326230998205,
|
||||||
);
|
);
|
||||||
// drag line from first segment midpoint
|
// drag line from first segment midpoint
|
||||||
drag(
|
drag(
|
||||||
firstSegmentMidpoint,
|
firstSegmentMidpoint,
|
||||||
point(
|
pointFrom(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -1151,7 +1160,7 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Drag from last point
|
// Drag from last point
|
||||||
drag(points[1], point(points[1][0] + 300, points[1][1]));
|
drag(points[1], pointFrom(points[1][0] + 300, points[1][1]));
|
||||||
|
|
||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
@@ -1350,11 +1359,11 @@ describe("Test Linear Elements", () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
point: point(line.points[0][0] + 10, line.points[0][1] + 10),
|
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: line.points.length - 1,
|
index: line.points.length - 1,
|
||||||
point: point(
|
point: pointFrom(
|
||||||
line.points[line.points.length - 1][0] - 10,
|
line.points[line.points.length - 1][0] - 10,
|
||||||
line.points[line.points.length - 1][1] - 10,
|
line.points[line.points.length - 1][1] - 10,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { isLinearElement } from "../element/typeChecks";
|
|||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { point } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -220,12 +220,17 @@ describe("generic element", () => {
|
|||||||
|
|
||||||
describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
||||||
const points: Record<typeof type, LocalPoint[]> = {
|
const points: Record<typeof type, LocalPoint[]> = {
|
||||||
line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)],
|
line: [
|
||||||
|
pointFrom(0, 0),
|
||||||
|
pointFrom(60, -20),
|
||||||
|
pointFrom(20, 40),
|
||||||
|
pointFrom(-40, 0),
|
||||||
|
],
|
||||||
freedraw: [
|
freedraw: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(-2.474600807561444, 41.021700699972),
|
pointFrom(-2.474600807561444, 41.021700699972),
|
||||||
point(3.6627956000014024, 47.84174560617245),
|
pointFrom(3.6627956000014024, 47.84174560617245),
|
||||||
point(40.495224145598115, 47.15909710753482),
|
pointFrom(40.495224145598115, 47.15909710753482),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,11 +298,11 @@ describe("arrow element", () => {
|
|||||||
it("resizes with a label", async () => {
|
it("resizes with a label", async () => {
|
||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(40, 140),
|
pointFrom(40, 140),
|
||||||
point(80, 60), // label's anchor
|
pointFrom(80, 60), // label's anchor
|
||||||
point(180, 20),
|
pointFrom(180, 20),
|
||||||
point(200, 120),
|
pointFrom(200, 120),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const label = await UI.editText(arrow, "Hello");
|
const label = await UI.editText(arrow, "Hello");
|
||||||
@@ -747,24 +752,24 @@ describe("multiple selection", () => {
|
|||||||
x: 60,
|
x: 60,
|
||||||
y: 40,
|
y: 40,
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(-40, 40),
|
pointFrom(-40, 40),
|
||||||
point(-60, 0),
|
pointFrom(-60, 0),
|
||||||
point(0, -40),
|
pointFrom(0, -40),
|
||||||
point(40, 20),
|
pointFrom(40, 20),
|
||||||
point(0, 40),
|
pointFrom(0, 40),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const freedraw = UI.createElement("freedraw", {
|
const freedraw = UI.createElement("freedraw", {
|
||||||
x: 63.56072661326618,
|
x: 63.56072661326618,
|
||||||
y: 100,
|
y: 100,
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(-43.56072661326618, 18.15048126846341),
|
pointFrom(-43.56072661326618, 18.15048126846341),
|
||||||
point(-43.56072661326618, 29.041198460587566),
|
pointFrom(-43.56072661326618, 29.041198460587566),
|
||||||
point(-38.115368017204105, 42.652452795512204),
|
pointFrom(-38.115368017204105, 42.652452795512204),
|
||||||
point(-19.964886748740696, 66.24829266003775),
|
pointFrom(-19.964886748740696, 66.24829266003775),
|
||||||
point(19.056612930986716, 77.1390098521619),
|
pointFrom(19.056612930986716, 77.1390098521619),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1101,13 +1106,13 @@ describe("multiple selection", () => {
|
|||||||
x: 60,
|
x: 60,
|
||||||
y: 0,
|
y: 0,
|
||||||
points: [
|
points: [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(-40, 40),
|
pointFrom(-40, 40),
|
||||||
point(-20, 60),
|
pointFrom(-20, 60),
|
||||||
point(20, 20),
|
pointFrom(20, 20),
|
||||||
point(40, 40),
|
pointFrom(40, 40),
|
||||||
point(-20, 100),
|
pointFrom(-20, 100),
|
||||||
point(-60, 60),
|
pointFrom(-60, 60),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,679 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
import fallbackLangData from "./helpers/locales/en.json";
|
||||||
|
import type {
|
||||||
|
SubtypeLoadedCb,
|
||||||
|
SubtypeRecord,
|
||||||
|
SubtypeMethods,
|
||||||
|
SubtypePrepFn,
|
||||||
|
} from "../element/subtypes";
|
||||||
|
import {
|
||||||
|
addSubtypeMethods,
|
||||||
|
ensureSubtypesLoadedForElements,
|
||||||
|
getSubtypeMethods,
|
||||||
|
getSubtypeNames,
|
||||||
|
hasAlwaysEnabledActions,
|
||||||
|
isValidSubtype,
|
||||||
|
selectSubtype,
|
||||||
|
subtypeCollides,
|
||||||
|
} from "../element/subtypes";
|
||||||
|
|
||||||
|
import { render } from "./test-utils";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
FontString,
|
||||||
|
Theme,
|
||||||
|
} from "../element/types";
|
||||||
|
import { createIcon, iconFillColor } from "../components/icons";
|
||||||
|
import { SubtypeButton } from "../components/Subtypes";
|
||||||
|
import type { LangLdr } from "../i18n";
|
||||||
|
import { registerCustomLangData, t } from "../i18n";
|
||||||
|
import { getFontString, getShortcutKey } from "../utils";
|
||||||
|
import * as textElementUtils from "../element/textElement";
|
||||||
|
import { isTextElement } from "../element";
|
||||||
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
import type { Action, ActionName } from "../actions/types";
|
||||||
|
import { makeCustomActionName } from "../actions/types";
|
||||||
|
import type { AppState } from "../types";
|
||||||
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
|
import { actionChangeSloppiness } from "../actions";
|
||||||
|
import { actionChangeRoundness } from "../actions/actionProperties";
|
||||||
|
|
||||||
|
const MW = 200;
|
||||||
|
const TWIDTH = 200;
|
||||||
|
const THEIGHT = 20;
|
||||||
|
const FONTSIZE = 20;
|
||||||
|
const DBFONTSIZE = 40;
|
||||||
|
const TRFONTSIZE = 60;
|
||||||
|
|
||||||
|
const getLangData: LangLdr = (langCode) =>
|
||||||
|
import(`./helpers/locales/${langCode}.json`);
|
||||||
|
|
||||||
|
const testSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
stroke={iconFillColor(theme)}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>,
|
||||||
|
{ width: 40, height: 20, mirror: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const TEST_ACTION = "testAction";
|
||||||
|
const TEST_DISABLE1 = actionChangeSloppiness;
|
||||||
|
const TEST_DISABLE3 = actionChangeRoundness;
|
||||||
|
|
||||||
|
const test1: SubtypeRecord = {
|
||||||
|
subtype: "test",
|
||||||
|
parents: ["line", "arrow", "rectangle", "diamond", "ellipse"],
|
||||||
|
disabledNames: [TEST_DISABLE1.name as ActionName],
|
||||||
|
actionNames: [TEST_ACTION],
|
||||||
|
};
|
||||||
|
const test1NonParent = "text" as const;
|
||||||
|
|
||||||
|
const test2: SubtypeRecord = {
|
||||||
|
subtype: "test2",
|
||||||
|
parents: ["text"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const test3: SubtypeRecord = {
|
||||||
|
subtype: "test3",
|
||||||
|
parents: ["text", "line"],
|
||||||
|
shortcutMap: {
|
||||||
|
testShortcut: [getShortcutKey("Shift+T")],
|
||||||
|
},
|
||||||
|
alwaysEnabledNames: ["test3Always"],
|
||||||
|
disabledNames: [TEST_DISABLE3.name as ActionName],
|
||||||
|
};
|
||||||
|
|
||||||
|
let testActions: Action[] | null = null;
|
||||||
|
|
||||||
|
const makeTestActions = () => {
|
||||||
|
if (testActions) {
|
||||||
|
return testActions;
|
||||||
|
}
|
||||||
|
const testAction: Action = {
|
||||||
|
name: makeCustomActionName(TEST_ACTION),
|
||||||
|
label: t("toolBar.test"),
|
||||||
|
trackEvent: false,
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
storeAction: "none",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
testActions = [
|
||||||
|
testAction,
|
||||||
|
SubtypeButton(test1.subtype, test1.parents[0], testSubtypeIcon),
|
||||||
|
SubtypeButton(test2.subtype, test2.parents[0], testSubtypeIcon),
|
||||||
|
SubtypeButton(test3.subtype, test3.parents[0], testSubtypeIcon),
|
||||||
|
];
|
||||||
|
return testActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanTestElementUpdate = function (updates) {
|
||||||
|
const oldUpdates = {};
|
||||||
|
for (const key in updates) {
|
||||||
|
if (key !== "roughness") {
|
||||||
|
(oldUpdates as any)[key] = (updates as any)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(updates as any).roughness = 0;
|
||||||
|
return oldUpdates;
|
||||||
|
} as SubtypeMethods["clean"];
|
||||||
|
|
||||||
|
const prepareNullSubtype = function () {
|
||||||
|
const methods = {} as SubtypeMethods;
|
||||||
|
methods.clean = cleanTestElementUpdate;
|
||||||
|
methods.measureText = measureTest2;
|
||||||
|
methods.wrapText = wrapTest2;
|
||||||
|
|
||||||
|
const actions = makeTestActions().filter((_, index) => index > 0);
|
||||||
|
return { actions, methods };
|
||||||
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
|
const prepareTest1Subtype = function (
|
||||||
|
addSubtypeAction,
|
||||||
|
addLangData,
|
||||||
|
onSubtypeLoaded,
|
||||||
|
) {
|
||||||
|
const methods = {} as SubtypeMethods;
|
||||||
|
methods.clean = cleanTestElementUpdate;
|
||||||
|
|
||||||
|
addLangData(fallbackLangData, getLangData);
|
||||||
|
registerCustomLangData(fallbackLangData, getLangData);
|
||||||
|
|
||||||
|
const actions = makeTestActions().filter((_, index) => index < 2);
|
||||||
|
actions.forEach((action) => addSubtypeAction(action));
|
||||||
|
|
||||||
|
return { actions, methods };
|
||||||
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
|
let test2Loaded = false;
|
||||||
|
|
||||||
|
const ensureLoadedTest2: SubtypeMethods["ensureLoaded"] = async (callback) => {
|
||||||
|
test2Loaded = true;
|
||||||
|
if (onTest2Loaded) {
|
||||||
|
onTest2Loaded((el) => isTextElement(el) && el.subtype === test2.subtype);
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
||||||
|
const text = next?.text ?? element.text;
|
||||||
|
const customData = next?.customData ?? {};
|
||||||
|
const fontSize = customData.triple
|
||||||
|
? TRFONTSIZE
|
||||||
|
: next?.fontSize ?? element.fontSize;
|
||||||
|
const fontFamily = element.fontFamily;
|
||||||
|
const fontString = getFontString({ fontSize, fontFamily });
|
||||||
|
const lineHeight = element.lineHeight;
|
||||||
|
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
||||||
|
const width = test2Loaded
|
||||||
|
? metrics.width * 2
|
||||||
|
: Math.max(metrics.width - 10, 0);
|
||||||
|
const height = test2Loaded
|
||||||
|
? metrics.height * 2
|
||||||
|
: Math.max(metrics.height - 5, 0);
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapTest2: SubtypeMethods["wrapText"] = function (
|
||||||
|
element,
|
||||||
|
maxWidth,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
const text = next?.text ?? element.originalText;
|
||||||
|
if (next?.customData && next?.customData.triple === true) {
|
||||||
|
return `${text.split(" ").join("\n")}\nHELLO WORLD.`;
|
||||||
|
}
|
||||||
|
if (next?.fontSize === DBFONTSIZE) {
|
||||||
|
return `${text.split(" ").join("\n")}\nHELLO World.`;
|
||||||
|
}
|
||||||
|
return `${text.split(" ").join("\n")}\nHello world.`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let onTest2Loaded: SubtypeLoadedCb | undefined;
|
||||||
|
|
||||||
|
const prepareTest2Subtype = function (
|
||||||
|
addSubtypeAction,
|
||||||
|
addLangData,
|
||||||
|
onSubtypeLoaded,
|
||||||
|
) {
|
||||||
|
const methods = {
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
|
measureText: measureTest2,
|
||||||
|
wrapText: wrapTest2,
|
||||||
|
} as SubtypeMethods;
|
||||||
|
|
||||||
|
addLangData(fallbackLangData, getLangData);
|
||||||
|
registerCustomLangData(fallbackLangData, getLangData);
|
||||||
|
|
||||||
|
const actions = [makeTestActions()[2]];
|
||||||
|
actions.forEach((action) => addSubtypeAction(action));
|
||||||
|
|
||||||
|
onTest2Loaded = onSubtypeLoaded;
|
||||||
|
|
||||||
|
return { actions, methods };
|
||||||
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
|
const prepareTest3Subtype = function (
|
||||||
|
addSubtypeAction,
|
||||||
|
addLangData,
|
||||||
|
onSubtypeLoaded,
|
||||||
|
) {
|
||||||
|
const methods = {} as SubtypeMethods;
|
||||||
|
|
||||||
|
addLangData(fallbackLangData, getLangData);
|
||||||
|
registerCustomLangData(fallbackLangData, getLangData);
|
||||||
|
|
||||||
|
const actions = [makeTestActions()[3]];
|
||||||
|
actions.forEach((action) => addSubtypeAction(action));
|
||||||
|
|
||||||
|
return { actions, methods };
|
||||||
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("subtype registration", () => {
|
||||||
|
it("should check for invalid subtype or parents", async () => {
|
||||||
|
await render(<Excalidraw />, {});
|
||||||
|
// Define invalid subtype records
|
||||||
|
const null1 = {} as SubtypeRecord;
|
||||||
|
const null2 = { subtype: "" } as SubtypeRecord;
|
||||||
|
const null3 = { subtype: "null" } as SubtypeRecord;
|
||||||
|
const null4 = { subtype: "null", parents: [] } as SubtypeRecord;
|
||||||
|
// Try registering the invalid subtypes
|
||||||
|
const prepN1 = API.addSubtype(null1, prepareNullSubtype);
|
||||||
|
const prepN2 = API.addSubtype(null2, prepareNullSubtype);
|
||||||
|
const prepN3 = API.addSubtype(null3, prepareNullSubtype);
|
||||||
|
const prepN4 = API.addSubtype(null4, prepareNullSubtype);
|
||||||
|
// Verify the guards in `prepareSubtype` worked
|
||||||
|
expect(prepN1).toStrictEqual({ actions: null, methods: {} });
|
||||||
|
expect(prepN2).toStrictEqual({ actions: null, methods: {} });
|
||||||
|
expect(prepN3).toStrictEqual({ actions: null, methods: {} });
|
||||||
|
expect(prepN4).toStrictEqual({ actions: null, methods: {} });
|
||||||
|
});
|
||||||
|
it("should return subtype actions and methods correctly", async () => {
|
||||||
|
// Check initial registration works
|
||||||
|
let prep1 = API.addSubtype(test1, prepareTest1Subtype);
|
||||||
|
const actions = makeTestActions().filter((_, index) => index < 2);
|
||||||
|
expect(prep1.actions).toStrictEqual(actions);
|
||||||
|
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
||||||
|
// Check repeat registration fails
|
||||||
|
prep1 = API.addSubtype(test1, prepareNullSubtype);
|
||||||
|
expect(prep1.actions).toBeNull();
|
||||||
|
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
||||||
|
|
||||||
|
// Check initial registration works
|
||||||
|
let prep2 = API.addSubtype(test2, prepareTest2Subtype);
|
||||||
|
expect(prep2.actions).toStrictEqual([makeTestActions()[2]]);
|
||||||
|
expect(prep2.methods).toStrictEqual({
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
|
measureText: measureTest2,
|
||||||
|
wrapText: wrapTest2,
|
||||||
|
});
|
||||||
|
// Check repeat registration fails
|
||||||
|
prep2 = API.addSubtype(test2, prepareNullSubtype);
|
||||||
|
expect(prep2.actions).toBeNull();
|
||||||
|
expect(prep2.methods).toStrictEqual({
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
|
measureText: measureTest2,
|
||||||
|
wrapText: wrapTest2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check initial registration works
|
||||||
|
let prep3 = API.addSubtype(test3, prepareTest3Subtype);
|
||||||
|
expect(prep3.actions).toStrictEqual([makeTestActions()[3]]);
|
||||||
|
expect(prep3.methods).toStrictEqual({});
|
||||||
|
// Check repeat registration fails
|
||||||
|
prep3 = API.addSubtype(test3, prepareNullSubtype);
|
||||||
|
expect(prep3.actions).toBeNull();
|
||||||
|
expect(prep3.methods).toStrictEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("subtypes", () => {
|
||||||
|
it("should correctly register", async () => {
|
||||||
|
const subtypes = getSubtypeNames();
|
||||||
|
expect(subtypes).toContain(test1.subtype);
|
||||||
|
expect(subtypes).toContain(test2.subtype);
|
||||||
|
expect(subtypes).toContain(test3.subtype);
|
||||||
|
});
|
||||||
|
it("should return subtype methods", async () => {
|
||||||
|
expect(getSubtypeMethods(undefined)).toBeUndefined();
|
||||||
|
const test1Methods = getSubtypeMethods(test1.subtype);
|
||||||
|
expect(test1Methods?.clean).toBeDefined();
|
||||||
|
expect(test1Methods?.render).toBeUndefined();
|
||||||
|
expect(test1Methods?.wrapText).toBeUndefined();
|
||||||
|
expect(test1Methods?.renderSvg).toBeUndefined();
|
||||||
|
expect(test1Methods?.measureText).toBeUndefined();
|
||||||
|
expect(test1Methods?.ensureLoaded).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("should not overwrite subtype methods", async () => {
|
||||||
|
addSubtypeMethods(test1.subtype, {});
|
||||||
|
addSubtypeMethods(test2.subtype, {});
|
||||||
|
addSubtypeMethods(test3.subtype, { clean: cleanTestElementUpdate });
|
||||||
|
const test1Methods = getSubtypeMethods(test1.subtype);
|
||||||
|
expect(test1Methods?.clean).toBeDefined();
|
||||||
|
const test2Methods = getSubtypeMethods(test2.subtype);
|
||||||
|
expect(test2Methods?.measureText).toBeDefined();
|
||||||
|
expect(test2Methods?.wrapText).toBeDefined();
|
||||||
|
const test3Methods = getSubtypeMethods(test3.subtype);
|
||||||
|
expect(test3Methods?.clean).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("should register custom shortcuts", async () => {
|
||||||
|
expect(
|
||||||
|
getShortcutFromShortcutName(makeCustomActionName("testShortcut")),
|
||||||
|
).toBe("Shift+T");
|
||||||
|
});
|
||||||
|
it("should correctly validate", async () => {
|
||||||
|
test1.parents.forEach((p) => {
|
||||||
|
expect(isValidSubtype(test1.subtype, p)).toBe(true);
|
||||||
|
expect(isValidSubtype(undefined, p)).toBe(false);
|
||||||
|
});
|
||||||
|
expect(isValidSubtype(test1.subtype, test1NonParent)).toBe(false);
|
||||||
|
expect(isValidSubtype(test1.subtype, undefined)).toBe(false);
|
||||||
|
expect(isValidSubtype(undefined, undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
it("should collide with themselves", async () => {
|
||||||
|
expect(subtypeCollides(test1.subtype, [test1.subtype])).toBe(true);
|
||||||
|
expect(subtypeCollides(test1.subtype, [test1.subtype, test2.subtype])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("should not collide without type overlap", async () => {
|
||||||
|
expect(subtypeCollides(test1.subtype, [test2.subtype])).toBe(false);
|
||||||
|
});
|
||||||
|
it("should collide with type overlap", async () => {
|
||||||
|
expect(subtypeCollides(test1.subtype, [test3.subtype])).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to ExcalidrawElements", async () => {
|
||||||
|
const elements = [
|
||||||
|
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
||||||
|
API.createElement({ type: "arrow", id: "B", subtype: test1.subtype }),
|
||||||
|
API.createElement({ type: "rectangle", id: "C", subtype: test1.subtype }),
|
||||||
|
API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }),
|
||||||
|
API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||||
|
elements.forEach((el) => expect(el.subtype).toBe(test1.subtype));
|
||||||
|
});
|
||||||
|
it("should enforce prop value restrictions", async () => {
|
||||||
|
const elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "line",
|
||||||
|
id: "A",
|
||||||
|
subtype: test1.subtype,
|
||||||
|
roughness: 1,
|
||||||
|
}),
|
||||||
|
API.createElement({ type: "line", id: "B", roughness: 1 }),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (el.subtype === test1.subtype) {
|
||||||
|
expect(el.roughness).toBe(0);
|
||||||
|
} else {
|
||||||
|
expect(el.roughness).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should consider enforced prop values in version increments", async () => {
|
||||||
|
const rectA = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
id: "A",
|
||||||
|
subtype: test1.subtype,
|
||||||
|
roughness: 1,
|
||||||
|
strokeWidth: 1,
|
||||||
|
});
|
||||||
|
const rectB = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
id: "B",
|
||||||
|
subtype: test1.subtype,
|
||||||
|
roughness: 1,
|
||||||
|
strokeWidth: 1,
|
||||||
|
});
|
||||||
|
// Initial element creation checks
|
||||||
|
expect(rectA.roughness).toBe(0);
|
||||||
|
expect(rectB.roughness).toBe(0);
|
||||||
|
expect(rectA.version).toBe(1);
|
||||||
|
expect(rectB.version).toBe(1);
|
||||||
|
// Check that attempting to set prop values not permitted by the subtype
|
||||||
|
// doesn't increment element versions
|
||||||
|
mutateElement(rectA, { roughness: 2 });
|
||||||
|
mutateElement(rectB, { roughness: 2, strokeWidth: 2 });
|
||||||
|
expect(rectA.version).toBe(1);
|
||||||
|
expect(rectB.version).toBe(2);
|
||||||
|
// Check that element versions don't increment when creating new elements
|
||||||
|
// while attempting to use prop values not permitted by the subtype
|
||||||
|
// First check based on `rectA` (unsuccessfully mutated)
|
||||||
|
const rectC = newElementWith(rectA, { roughness: 1 });
|
||||||
|
const rectD = newElementWith(rectA, { roughness: 1, strokeWidth: 1.5 });
|
||||||
|
expect(rectC.version).toBe(1);
|
||||||
|
expect(rectD.version).toBe(2);
|
||||||
|
// Then check based on `rectB` (successfully mutated)
|
||||||
|
const rectE = newElementWith(rectB, { roughness: 1 });
|
||||||
|
const rectF = newElementWith(rectB, { roughness: 1, strokeWidth: 1.5 });
|
||||||
|
expect(rectE.version).toBe(2);
|
||||||
|
expect(rectF.version).toBe(3);
|
||||||
|
});
|
||||||
|
it("should call custom text methods", async () => {
|
||||||
|
const testString = "A quick brown fox jumps over the lazy dog.";
|
||||||
|
const elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "A",
|
||||||
|
subtype: test2.subtype,
|
||||||
|
text: testString,
|
||||||
|
fontSize: FONTSIZE,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||||
|
const mockMeasureText = (text: string, font: FontString) => {
|
||||||
|
if (text === testString) {
|
||||||
|
let multiplier = 1;
|
||||||
|
if (font.includes(`${DBFONTSIZE}`)) {
|
||||||
|
multiplier = 2;
|
||||||
|
}
|
||||||
|
if (font.includes(`${TRFONTSIZE}`)) {
|
||||||
|
multiplier = 3;
|
||||||
|
}
|
||||||
|
const width = multiplier * TWIDTH;
|
||||||
|
const height = multiplier * THEIGHT;
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
return { width: 1, height: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.spyOn(textElementUtils, "measureText").mockImplementation(
|
||||||
|
mockMeasureText,
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (isTextElement(el)) {
|
||||||
|
// First test with `ExcalidrawTextElement.text`
|
||||||
|
const metrics = textElementUtils.measureTextElement(el);
|
||||||
|
expect(metrics).toStrictEqual({
|
||||||
|
width: TWIDTH - 10,
|
||||||
|
height: THEIGHT - 5,
|
||||||
|
});
|
||||||
|
const wrappedText = textElementUtils.wrapTextElement(el, MW);
|
||||||
|
expect(wrappedText).toEqual(
|
||||||
|
`${testString.split(" ").join("\n")}\nHello world.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now test with modified text in `next`
|
||||||
|
let next: {
|
||||||
|
text?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
customData?: Record<string, any>;
|
||||||
|
} = {
|
||||||
|
text: "Hello world.",
|
||||||
|
};
|
||||||
|
const nextMetrics = textElementUtils.measureTextElement(el, next);
|
||||||
|
expect(nextMetrics).toStrictEqual({ width: 0, height: 0 });
|
||||||
|
const nextWrappedText = textElementUtils.wrapTextElement(el, MW, next);
|
||||||
|
expect(nextWrappedText).toEqual("Hello\nworld.\nHello world.");
|
||||||
|
|
||||||
|
// Now test modified fontSizes in `next`
|
||||||
|
next = { fontSize: DBFONTSIZE };
|
||||||
|
const nextFM = textElementUtils.measureTextElement(el, next);
|
||||||
|
expect(nextFM).toStrictEqual({
|
||||||
|
width: 2 * TWIDTH - 10,
|
||||||
|
height: 2 * THEIGHT - 5,
|
||||||
|
});
|
||||||
|
const nextFWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||||
|
expect(nextFWrText).toEqual(
|
||||||
|
`${testString.split(" ").join("\n")}\nHELLO World.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now test customData in `next`
|
||||||
|
next = { customData: { triple: true } };
|
||||||
|
const nextCD = textElementUtils.measureTextElement(el, next);
|
||||||
|
expect(nextCD).toStrictEqual({
|
||||||
|
width: 3 * TWIDTH - 10,
|
||||||
|
height: 3 * THEIGHT - 5,
|
||||||
|
});
|
||||||
|
const nextCDWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||||
|
expect(nextCDWrText).toEqual(
|
||||||
|
`${testString.split(" ").join("\n")}\nHELLO WORLD.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should recognize subtypes with always-enabled actions", async () => {
|
||||||
|
expect(hasAlwaysEnabledActions(test1.subtype)).toBe(false);
|
||||||
|
expect(hasAlwaysEnabledActions(test2.subtype)).toBe(false);
|
||||||
|
expect(hasAlwaysEnabledActions(test3.subtype)).toBe(true);
|
||||||
|
});
|
||||||
|
it("should select active subtypes and customData", async () => {
|
||||||
|
const appState = {} as {
|
||||||
|
activeSubtypes: AppState["activeSubtypes"];
|
||||||
|
customData: AppState["customData"];
|
||||||
|
};
|
||||||
|
|
||||||
|
// No active subtypes
|
||||||
|
let subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.subtype).toBeUndefined();
|
||||||
|
expect(subtypes.customData).toBeUndefined();
|
||||||
|
// Subtype for both "text" and "line" types
|
||||||
|
appState.activeSubtypes = [test3.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.subtype).toBe(test3.subtype);
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.subtype).toBe(test3.subtype);
|
||||||
|
subtypes = selectSubtype(appState, "arrow");
|
||||||
|
expect(subtypes.subtype).toBeUndefined();
|
||||||
|
// Subtype for multiple linear types
|
||||||
|
appState.activeSubtypes = [test1.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.subtype).toBeUndefined();
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.subtype).toBe(test1.subtype);
|
||||||
|
subtypes = selectSubtype(appState, "arrow");
|
||||||
|
expect(subtypes.subtype).toBe(test1.subtype);
|
||||||
|
// Subtype for "text" only
|
||||||
|
appState.activeSubtypes = [test2.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.subtype).toBe(test2.subtype);
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.subtype).toBeUndefined();
|
||||||
|
subtypes = selectSubtype(appState, "arrow");
|
||||||
|
expect(subtypes.subtype).toBeUndefined();
|
||||||
|
|
||||||
|
// Test customData
|
||||||
|
appState.customData = {};
|
||||||
|
appState.customData[test1.subtype] = { test: true };
|
||||||
|
appState.customData[test2.subtype] = { test2: true };
|
||||||
|
appState.customData[test3.subtype] = { test3: true };
|
||||||
|
// Subtype for both "text" and "line" types
|
||||||
|
appState.activeSubtypes = [test3.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.customData).toBeDefined();
|
||||||
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test3.subtype]).toBe(true);
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.customData).toBeDefined();
|
||||||
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test3.subtype]).toBe(true);
|
||||||
|
subtypes = selectSubtype(appState, "arrow");
|
||||||
|
expect(subtypes.customData).toBeUndefined();
|
||||||
|
// Subtype for multiple linear types
|
||||||
|
appState.activeSubtypes = [test1.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.customData).toBeUndefined();
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.customData).toBeDefined();
|
||||||
|
expect(subtypes.customData![test1.subtype]).toBe(true);
|
||||||
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||||
|
// Multiple, non-colliding subtypes
|
||||||
|
appState.activeSubtypes = [test1.subtype, test2.subtype];
|
||||||
|
subtypes = selectSubtype(appState, "text");
|
||||||
|
expect(subtypes.customData).toBeDefined();
|
||||||
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test2.subtype]).toBe(true);
|
||||||
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||||
|
subtypes = selectSubtype(appState, "line");
|
||||||
|
expect(subtypes.customData).toBeDefined();
|
||||||
|
expect(subtypes.customData![test1.subtype]).toBe(true);
|
||||||
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||||
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("subtype actions", () => {
|
||||||
|
let elements: ExcalidrawElement[];
|
||||||
|
beforeEach(async () => {
|
||||||
|
elements = [
|
||||||
|
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
||||||
|
API.createElement({ type: "line", id: "B" }),
|
||||||
|
API.createElement({ type: "line", id: "C", subtype: test3.subtype }),
|
||||||
|
API.createElement({ type: "text", id: "D", subtype: test3.subtype }),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||||
|
});
|
||||||
|
it("should apply to elements with their subtype", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
||||||
|
});
|
||||||
|
it("should apply to elements without a subtype", async () => {
|
||||||
|
h.setState({ selectedElementIds: { B: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to elements with and without their subtype", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true, B: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to elements with a different subtype", async () => {
|
||||||
|
h.setState({ selectedElementIds: { C: true, D: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(false);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to like types with varying subtypes", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true, C: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to non-like types with varying subtypes", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true, D: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
||||||
|
});
|
||||||
|
it("should apply to like/non-like types with varying subtypes", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true, B: true, D: true } });
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
expect(am.isActionEnabled(makeTestActions()[0], { elements })).toBe(true);
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
it("should apply to the correct parent type", async () => {
|
||||||
|
const am = h.app.actionManager;
|
||||||
|
h.setState({ selectedElementIds: { A: true, C: true } });
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
||||||
|
h.setState({ selectedElementIds: { A: true, D: true } });
|
||||||
|
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("subtype loading", () => {
|
||||||
|
let elements: ExcalidrawElement[];
|
||||||
|
beforeEach(async () => {
|
||||||
|
const testString = "A quick brown fox jumps over the lazy dog.";
|
||||||
|
elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "A",
|
||||||
|
subtype: test2.subtype,
|
||||||
|
text: testString,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||||
|
h.elements = elements;
|
||||||
|
});
|
||||||
|
it("should redraw text bounding boxes", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true } });
|
||||||
|
const el = h.elements[0] as ExcalidrawTextElement;
|
||||||
|
expect(el.width).toEqual(100);
|
||||||
|
expect(el.height).toEqual(100);
|
||||||
|
ensureSubtypesLoadedForElements(elements);
|
||||||
|
expect(el.width).toEqual(TWIDTH * 2);
|
||||||
|
expect(el.height).toEqual(THEIGHT * 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,6 +35,12 @@ import type { ClipboardData } from "./clipboard";
|
|||||||
import type { isOverScrollBars } from "./scene/scrollbars";
|
import type { isOverScrollBars } from "./scene/scrollbars";
|
||||||
import type { MaybeTransformHandleType } from "./element/transformHandles";
|
import type { MaybeTransformHandleType } from "./element/transformHandles";
|
||||||
import type Library from "./data/library";
|
import type Library from "./data/library";
|
||||||
|
import type {
|
||||||
|
SubtypeMethods,
|
||||||
|
Subtype,
|
||||||
|
SubtypePrepFn,
|
||||||
|
SubtypeRecord,
|
||||||
|
} from "./element/subtypes";
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import type { ContextMenuItems } from "./components/ContextMenu";
|
import type { ContextMenuItems } from "./components/ContextMenu";
|
||||||
@@ -271,6 +277,10 @@ export interface AppState {
|
|||||||
*/
|
*/
|
||||||
editingTextElement: NonDeletedExcalidrawElement | null;
|
editingTextElement: NonDeletedExcalidrawElement | null;
|
||||||
editingLinearElement: LinearElementEditor | null;
|
editingLinearElement: LinearElementEditor | null;
|
||||||
|
activeSubtypes?: Subtype[];
|
||||||
|
customData?: {
|
||||||
|
[subtype: Subtype]: ExcalidrawElement["customData"];
|
||||||
|
};
|
||||||
activeTool: {
|
activeTool: {
|
||||||
/**
|
/**
|
||||||
* indicates a previous tool we should revert back to if we deselect the
|
* indicates a previous tool we should revert back to if we deselect the
|
||||||
@@ -739,6 +749,10 @@ export interface ExcalidrawImperativeAPI {
|
|||||||
getName: InstanceType<typeof App>["getName"];
|
getName: InstanceType<typeof App>["getName"];
|
||||||
scrollToContent: InstanceType<typeof App>["scrollToContent"];
|
scrollToContent: InstanceType<typeof App>["scrollToContent"];
|
||||||
registerAction: (action: Action) => void;
|
registerAction: (action: Action) => void;
|
||||||
|
addSubtype: (
|
||||||
|
record: SubtypeRecord,
|
||||||
|
subtypePrepFn: SubtypePrepFn,
|
||||||
|
) => { actions: readonly Action[] | null; methods: Partial<SubtypeMethods> };
|
||||||
refresh: InstanceType<typeof App>["refresh"];
|
refresh: InstanceType<typeof App>["refresh"];
|
||||||
setToast: InstanceType<typeof App>["setToast"];
|
setToast: InstanceType<typeof App>["setToast"];
|
||||||
addFiles: (data: BinaryFileData[]) => void;
|
addFiles: (data: BinaryFileData[]) => void;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
|
import {
|
||||||
|
isLineSegment,
|
||||||
|
lineSegment,
|
||||||
|
pointFrom,
|
||||||
|
type GlobalPoint,
|
||||||
|
} from "../math";
|
||||||
import type { LineSegment } from "../utils";
|
import type { LineSegment } from "../utils";
|
||||||
import type { BoundingBox, Bounds } from "./element/bounds";
|
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||||
import { isBounds } from "./element/typeChecks";
|
import { isBounds } from "./element/typeChecks";
|
||||||
@@ -52,8 +57,8 @@ export const debugDrawPoint = (
|
|||||||
|
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
|
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
|
||||||
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
@@ -62,8 +67,8 @@ export const debugDrawPoint = (
|
|||||||
);
|
);
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
|
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
|
||||||
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
@@ -83,20 +88,20 @@ export const debugDrawBoundingBox = (
|
|||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox.minX, bbox.minY),
|
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
point<GlobalPoint>(bbox.minX, bbox.minY),
|
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
@@ -118,20 +123,20 @@ export const debugDrawBounds = (
|
|||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox[0], bbox[1]),
|
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
point<GlobalPoint>(bbox[2], bbox[1]),
|
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox[2], bbox[1]),
|
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
point<GlobalPoint>(bbox[2], bbox[3]),
|
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox[2], bbox[3]),
|
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
point<GlobalPoint>(bbox[0], bbox[3]),
|
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
point<GlobalPoint>(bbox[0], bbox[3]),
|
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
point<GlobalPoint>(bbox[0], bbox[1]),
|
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isPointOnSymmetricArc } from "./arc";
|
import { isPointOnSymmetricArc } from "./arc";
|
||||||
import { point } from "./point";
|
import { pointFrom } from "./point";
|
||||||
|
|
||||||
describe("point on arc", () => {
|
describe("point on arc", () => {
|
||||||
it("should detect point on simple arc", () => {
|
it("should detect point on simple arc", () => {
|
||||||
@@ -10,7 +10,7 @@ describe("point on arc", () => {
|
|||||||
startAngle: -Math.PI / 4,
|
startAngle: -Math.PI / 4,
|
||||||
endAngle: Math.PI / 4,
|
endAngle: Math.PI / 4,
|
||||||
},
|
},
|
||||||
point(0.92291667, 0.385),
|
pointFrom(0.92291667, 0.385),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ describe("point on arc", () => {
|
|||||||
startAngle: -Math.PI / 4,
|
startAngle: -Math.PI / 4,
|
||||||
endAngle: Math.PI / 4,
|
endAngle: Math.PI / 4,
|
||||||
},
|
},
|
||||||
point(-0.92291667, 0.385),
|
pointFrom(-0.92291667, 0.385),
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -34,7 +34,7 @@ describe("point on arc", () => {
|
|||||||
startAngle: -Math.PI / 4,
|
startAngle: -Math.PI / 4,
|
||||||
endAngle: Math.PI / 4,
|
endAngle: Math.PI / 4,
|
||||||
},
|
},
|
||||||
point(-0.5, 0.5),
|
pointFrom(-0.5, 0.5),
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
import { point, pointRotateRads } from "./point";
|
import { pointFrom, pointRotateRads } from "./point";
|
||||||
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,10 +43,10 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
|||||||
const out: Point[] = [];
|
const out: Point[] = [];
|
||||||
if (len === 3) {
|
if (len === 3) {
|
||||||
out.push(
|
out.push(
|
||||||
point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||||
point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const points: Point[] = [];
|
const points: Point[] = [];
|
||||||
@@ -59,19 +59,19 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
|||||||
}
|
}
|
||||||
const b: Point[] = [];
|
const b: Point[] = [];
|
||||||
const s = 1 - curveTightness;
|
const s = 1 - curveTightness;
|
||||||
out.push(point(points[0][0], points[0][1]));
|
out.push(pointFrom(points[0][0], points[0][1]));
|
||||||
for (let i = 1; i + 2 < points.length; i++) {
|
for (let i = 1; i + 2 < points.length; i++) {
|
||||||
const cachedVertArray = points[i];
|
const cachedVertArray = points[i];
|
||||||
b[0] = point(cachedVertArray[0], cachedVertArray[1]);
|
b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
|
||||||
b[1] = point(
|
b[1] = pointFrom(
|
||||||
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
||||||
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
||||||
);
|
);
|
||||||
b[2] = point(
|
b[2] = pointFrom(
|
||||||
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
||||||
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
||||||
);
|
);
|
||||||
b[3] = point(points[i + 1][0], points[i + 1][1]);
|
b[3] = pointFrom(points[i + 1][0], points[i + 1][1]);
|
||||||
out.push(b[1], b[2], b[3]);
|
out.push(b[1], b[2], b[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
||||||
Math.pow(t, 3) * p3[1];
|
Math.pow(t, 3) * p3[1];
|
||||||
|
|
||||||
return point(x, y);
|
return pointFrom(x, y);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { point, pointRotateRads } from "./point";
|
import { pointFrom, pointRotateRads } from "./point";
|
||||||
import type { Radians } from "./types";
|
import type { Radians } from "./types";
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("rotate", () => {
|
||||||
@@ -9,14 +9,14 @@ describe("rotate", () => {
|
|||||||
const y2 = 30;
|
const y2 = 30;
|
||||||
const angle = (Math.PI / 2) as Radians;
|
const angle = (Math.PI / 2) as Radians;
|
||||||
const [rotatedX, rotatedY] = pointRotateRads(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
point(x1, y1),
|
pointFrom(x1, y1),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||||
const res2 = pointRotateRads(
|
const res2 = pointRotateRads(
|
||||||
point(rotatedX, rotatedY),
|
pointFrom(rotatedX, rotatedY),
|
||||||
point(x2, y2),
|
pointFrom(x2, y2),
|
||||||
-angle as Radians,
|
-angle as Radians,
|
||||||
);
|
);
|
||||||
expect(res2).toEqual([x1, x2]);
|
expect(res2).toEqual([x1, x2]);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
|
|||||||
* @param y The Y coordinate
|
* @param y The Y coordinate
|
||||||
* @returns The branded and created point
|
* @returns The branded and created point
|
||||||
*/
|
*/
|
||||||
export function point<Point extends GlobalPoint | LocalPoint>(
|
export function pointFrom<Point extends GlobalPoint | LocalPoint>(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): Point {
|
): Point {
|
||||||
@@ -33,7 +33,7 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
|
|||||||
numberArray: number[],
|
numberArray: number[],
|
||||||
): Point | undefined {
|
): Point | undefined {
|
||||||
return numberArray.length === 2
|
return numberArray.length === 2
|
||||||
? point<Point>(numberArray[0], numberArray[1])
|
? pointFrom<Point>(numberArray[0], numberArray[1])
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
|
|||||||
[cx, cy]: Point,
|
[cx, cy]: Point,
|
||||||
angle: Radians,
|
angle: Radians,
|
||||||
): Point {
|
): Point {
|
||||||
return point(
|
return pointFrom(
|
||||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||||
);
|
);
|
||||||
@@ -146,7 +146,7 @@ export function pointTranslate<
|
|||||||
From extends GlobalPoint | LocalPoint,
|
From extends GlobalPoint | LocalPoint,
|
||||||
To extends GlobalPoint | LocalPoint,
|
To extends GlobalPoint | LocalPoint,
|
||||||
>(p: From, v: Vector = [0, 0] as Vector): To {
|
>(p: From, v: Vector = [0, 0] as Vector): To {
|
||||||
return point(p[0] + v[0], p[1] + v[1]);
|
return pointFrom(p[0] + v[0], p[1] + v[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,7 +157,7 @@ export function pointTranslate<
|
|||||||
* @returns The middle point
|
* @returns The middle point
|
||||||
*/
|
*/
|
||||||
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
||||||
return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +172,7 @@ export function pointAdd<Point extends LocalPoint | GlobalPoint>(
|
|||||||
a: Point,
|
a: Point,
|
||||||
b: Point,
|
b: Point,
|
||||||
): Point {
|
): Point {
|
||||||
return point(a[0] + b[0], a[1] + b[1]);
|
return pointFrom(a[0] + b[0], a[1] + b[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,7 +187,7 @@ export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
|
|||||||
a: Point,
|
a: Point,
|
||||||
b: Point,
|
b: Point,
|
||||||
): Point {
|
): Point {
|
||||||
return point(a[0] - b[0], a[1] - b[1]);
|
return pointFrom(a[0] - b[0], a[1] - b[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentRotate,
|
lineSegmentRotate,
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateDegs,
|
pointRotateDegs,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
import { pointOnCurve, pointOnPolyline } from "./collision";
|
import { pointOnCurve, pointOnPolyline } from "./collision";
|
||||||
@@ -12,21 +12,21 @@ import type { Polyline } from "./geometry/shape";
|
|||||||
|
|
||||||
describe("point and curve", () => {
|
describe("point and curve", () => {
|
||||||
const c: Curve<GlobalPoint> = curve(
|
const c: Curve<GlobalPoint> = curve(
|
||||||
point(1.4, 1.65),
|
pointFrom(1.4, 1.65),
|
||||||
point(1.9, 7.9),
|
pointFrom(1.9, 7.9),
|
||||||
point(5.9, 1.65),
|
pointFrom(5.9, 1.65),
|
||||||
point(6.44, 4.84),
|
pointFrom(6.44, 4.84),
|
||||||
);
|
);
|
||||||
|
|
||||||
it("point on curve", () => {
|
it("point on curve", () => {
|
||||||
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
||||||
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
|
expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true);
|
||||||
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
|
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
|
||||||
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
|
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false);
|
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
|
||||||
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
||||||
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -34,52 +34,52 @@ describe("point and curve", () => {
|
|||||||
|
|
||||||
describe("point and polylines", () => {
|
describe("point and polylines", () => {
|
||||||
const polyline: Polyline<GlobalPoint> = [
|
const polyline: Polyline<GlobalPoint> = [
|
||||||
lineSegment(point(1, 0), point(1, 2)),
|
lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
|
||||||
lineSegment(point(1, 2), point(2, 2)),
|
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
|
||||||
lineSegment(point(2, 2), point(2, 1)),
|
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
|
||||||
lineSegment(point(2, 1), point(3, 1)),
|
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
|
||||||
];
|
];
|
||||||
|
|
||||||
it("point on the line", () => {
|
it("point on the line", () => {
|
||||||
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
|
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
|
||||||
expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false);
|
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point on the line with rotation", () => {
|
it("point on the line with rotation", () => {
|
||||||
const truePoints = [
|
const truePoints = [
|
||||||
point(1, 0),
|
pointFrom(1, 0),
|
||||||
point(1, 2),
|
pointFrom(1, 2),
|
||||||
point(2, 2),
|
pointFrom(2, 2),
|
||||||
point(2, 1),
|
pointFrom(2, 1),
|
||||||
point(3, 1),
|
pointFrom(3, 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
truePoints.forEach((p) => {
|
truePoints.forEach((p) => {
|
||||||
const rotation = (Math.random() * 360) as Degrees;
|
const rotation = (Math.random() * 360) as Degrees;
|
||||||
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||||
const rotatedPolyline = polyline.map((line) =>
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||||
);
|
);
|
||||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const falsePoints = [point(0, 1), point(2.1, 1.5)];
|
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
|
||||||
|
|
||||||
falsePoints.forEach((p) => {
|
falsePoints.forEach((p) => {
|
||||||
const rotation = (Math.random() * 360) as Degrees;
|
const rotation = (Math.random() * 360) as Degrees;
|
||||||
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||||
const rotatedPolyline = polyline.map((line) =>
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||||
);
|
);
|
||||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import type { Curve } from "../math";
|
import type { Curve } from "../math";
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
point,
|
pointFrom,
|
||||||
polygonIncludesPoint,
|
polygonIncludesPoint,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
pointOnPolygon,
|
pointOnPolygon,
|
||||||
@@ -110,7 +110,7 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
for (let i = 0; i < segments; i++) {
|
for (let i = 0; i < segments; i++) {
|
||||||
t += increment;
|
t += increment;
|
||||||
if (t <= 1) {
|
if (t <= 1) {
|
||||||
const nextPoint: Point = point(equation(t, 0), equation(t, 1));
|
const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1));
|
||||||
lineSegments.push(lineSegment(startingPoint, nextPoint));
|
lineSegments.push(lineSegment(startingPoint, nextPoint));
|
||||||
startingPoint = nextPoint;
|
startingPoint = nextPoint;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
pointFrom,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
polygon,
|
polygon,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
@@ -23,93 +23,127 @@ describe("point and line", () => {
|
|||||||
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
const s: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(1, 0),
|
||||||
|
pointFrom(1, 2),
|
||||||
|
);
|
||||||
|
|
||||||
it("point on the line", () => {
|
it("point on the line", () => {
|
||||||
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
|
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
|
||||||
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
|
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
|
||||||
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
|
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and polygon", () => {
|
describe("point and polygon", () => {
|
||||||
const poly: Polygon<GlobalPoint> = polygon(
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
point(10, 10),
|
pointFrom(10, 10),
|
||||||
point(50, 10),
|
pointFrom(50, 10),
|
||||||
point(50, 50),
|
pointFrom(50, 50),
|
||||||
point(10, 50),
|
pointFrom(10, 50),
|
||||||
);
|
);
|
||||||
|
|
||||||
it("point on polygon", () => {
|
it("point on polygon", () => {
|
||||||
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
|
expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
|
expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
|
expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
|
expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
|
expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false);
|
||||||
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
|
expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point in polygon", () => {
|
it("point in polygon", () => {
|
||||||
const poly: Polygon<GlobalPoint> = polygon(
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(2, 0),
|
pointFrom(2, 0),
|
||||||
point(2, 2),
|
pointFrom(2, 2),
|
||||||
point(0, 2),
|
pointFrom(0, 2),
|
||||||
);
|
);
|
||||||
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
|
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
|
||||||
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and ellipse", () => {
|
describe("point and ellipse", () => {
|
||||||
const ellipse: Ellipse<GlobalPoint> = {
|
const ellipse: Ellipse<GlobalPoint> = {
|
||||||
center: point(0, 0),
|
center: pointFrom(0, 0),
|
||||||
angle: 0 as Radians,
|
angle: 0 as Radians,
|
||||||
halfWidth: 2,
|
halfWidth: 2,
|
||||||
halfHeight: 1,
|
halfHeight: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("point on ellipse", () => {
|
it("point on ellipse", () => {
|
||||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
[
|
||||||
|
pointFrom(0, 1),
|
||||||
|
pointFrom(0, -1),
|
||||||
|
pointFrom(2, 0),
|
||||||
|
pointFrom(-2, 0),
|
||||||
|
].forEach((p) => {
|
||||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||||
});
|
});
|
||||||
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
|
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
|
||||||
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
|
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point in ellipse", () => {
|
it("point in ellipse", () => {
|
||||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
[
|
||||||
|
pointFrom(0, 1),
|
||||||
|
pointFrom(0, -1),
|
||||||
|
pointFrom(2, 0),
|
||||||
|
pointFrom(-2, 0),
|
||||||
|
].forEach((p) => {
|
||||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
|
expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
|
||||||
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
|
expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
|
||||||
|
|
||||||
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
|
||||||
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("line and line", () => {
|
describe("line and line", () => {
|
||||||
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
||||||
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
pointFrom(1, 4),
|
||||||
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
pointFrom(3, 4),
|
||||||
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
);
|
||||||
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
const lineB: LineSegment<GlobalPoint> = lineSegment(
|
||||||
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
pointFrom(2, 1),
|
||||||
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
pointFrom(2, 7),
|
||||||
|
);
|
||||||
|
const lineC: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(1, 8),
|
||||||
|
pointFrom(3, 8),
|
||||||
|
);
|
||||||
|
const lineD: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(1, 8),
|
||||||
|
pointFrom(3, 8),
|
||||||
|
);
|
||||||
|
const lineE: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(1, 9),
|
||||||
|
pointFrom(3, 9),
|
||||||
|
);
|
||||||
|
const lineF: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(1, 2),
|
||||||
|
pointFrom(3, 4),
|
||||||
|
);
|
||||||
|
const lineG: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
pointFrom(0, 1),
|
||||||
|
pointFrom(2, 3),
|
||||||
|
);
|
||||||
|
|
||||||
it("intersection", () => {
|
it("intersection", () => {
|
||||||
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { Curve, LineSegment, Polygon, Radians } from "../../math";
|
|||||||
import {
|
import {
|
||||||
curve,
|
curve,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
point,
|
pointFrom,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointFromVector,
|
pointFromVector,
|
||||||
@@ -118,23 +118,23 @@ export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
const cx = x + width / 2;
|
const cx = x + width / 2;
|
||||||
const cy = y + height / 2;
|
const cy = y + height / 2;
|
||||||
|
|
||||||
const center: Point = point(cx, cy);
|
const center: Point = pointFrom(cx, cy);
|
||||||
|
|
||||||
let data: Polygon<Point>;
|
let data: Polygon<Point>;
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
data = polygon(
|
data = polygon(
|
||||||
pointRotateRads(point(cx, y), center, angle),
|
pointRotateRads(pointFrom(cx, y), center, angle),
|
||||||
pointRotateRads(point(x + width, cy), center, angle),
|
pointRotateRads(pointFrom(x + width, cy), center, angle),
|
||||||
pointRotateRads(point(cx, y + height), center, angle),
|
pointRotateRads(pointFrom(cx, y + height), center, angle),
|
||||||
pointRotateRads(point(x, cy), center, angle),
|
pointRotateRads(pointFrom(x, cy), center, angle),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = polygon(
|
data = polygon(
|
||||||
pointRotateRads(point(x, y), center, angle),
|
pointRotateRads(pointFrom(x, y), center, angle),
|
||||||
pointRotateRads(point(x + width, y), center, angle),
|
pointRotateRads(pointFrom(x + width, y), center, angle),
|
||||||
pointRotateRads(point(x + width, y + height), center, angle),
|
pointRotateRads(pointFrom(x + width, y + height), center, angle),
|
||||||
pointRotateRads(point(x, y + height), center, angle),
|
pointRotateRads(pointFrom(x, y + height), center, angle),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +162,11 @@ export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
y2 += padding;
|
y2 += padding;
|
||||||
|
|
||||||
//const angleInDegrees = angleToDegrees(element.angle);
|
//const angleInDegrees = angleToDegrees(element.angle);
|
||||||
const center = point(cx, cy);
|
const center = pointFrom(cx, cy);
|
||||||
const topLeft = pointRotateRads(point(x1, y1), center, element.angle);
|
const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle);
|
||||||
const topRight = pointRotateRads(point(x2, y1), center, element.angle);
|
const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle);
|
||||||
const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle);
|
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle);
|
||||||
const bottomRight = pointRotateRads(point(x2, y2), center, element.angle);
|
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
@@ -183,7 +183,7 @@ export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
return {
|
return {
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
data: {
|
data: {
|
||||||
center: point(x + width / 2, y + height / 2),
|
center: pointFrom(x + width / 2, y + height / 2),
|
||||||
angle,
|
angle,
|
||||||
halfWidth: width / 2,
|
halfWidth: width / 2,
|
||||||
halfHeight: height / 2,
|
halfHeight: height / 2,
|
||||||
@@ -203,20 +203,20 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
|||||||
// linear
|
// linear
|
||||||
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
startingPoint: Point = point(0, 0),
|
startingPoint: Point = pointFrom(0, 0),
|
||||||
angleInRadian: Radians,
|
angleInRadian: Radians,
|
||||||
center: Point,
|
center: Point,
|
||||||
): GeometricShape<Point> => {
|
): GeometricShape<Point> => {
|
||||||
const transform = (p: Point): Point =>
|
const transform = (p: Point): Point =>
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||||
center,
|
center,
|
||||||
angleInRadian,
|
angleInRadian,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ops = getCurvePathOps(roughShape);
|
const ops = getCurvePathOps(roughShape);
|
||||||
const polycurve: Polycurve<Point> = [];
|
const polycurve: Polycurve<Point> = [];
|
||||||
let p0 = point<Point>(0, 0);
|
let p0 = pointFrom<Point>(0, 0);
|
||||||
|
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
if (op.op === "move") {
|
if (op.op === "move") {
|
||||||
@@ -225,9 +225,9 @@ export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
p0 = transform(p);
|
p0 = transform(p);
|
||||||
}
|
}
|
||||||
if (op.op === "bcurveTo") {
|
if (op.op === "bcurveTo") {
|
||||||
const p1 = transform(point<Point>(op.data[0], op.data[1]));
|
const p1 = transform(pointFrom<Point>(op.data[0], op.data[1]));
|
||||||
const p2 = transform(point<Point>(op.data[2], op.data[3]));
|
const p2 = transform(pointFrom<Point>(op.data[2], op.data[3]));
|
||||||
const p3 = transform(point<Point>(op.data[4], op.data[5]));
|
const p3 = transform(pointFrom<Point>(op.data[4], op.data[5]));
|
||||||
polycurve.push(curve<Point>(p0, p1, p2, p3));
|
polycurve.push(curve<Point>(p0, p1, p2, p3));
|
||||||
p0 = p3;
|
p0 = p3;
|
||||||
}
|
}
|
||||||
@@ -288,13 +288,13 @@ export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
startingPoint: Point = point<Point>(0, 0),
|
startingPoint: Point = pointFrom<Point>(0, 0),
|
||||||
angleInRadian: Radians,
|
angleInRadian: Radians,
|
||||||
center: Point,
|
center: Point,
|
||||||
): GeometricShape<Point> => {
|
): GeometricShape<Point> => {
|
||||||
const transform = (p: Point) =>
|
const transform = (p: Point) =>
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||||
center,
|
center,
|
||||||
angleInRadian,
|
angleInRadian,
|
||||||
);
|
);
|
||||||
@@ -316,17 +316,17 @@ export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
if (operation.op === "move") {
|
if (operation.op === "move") {
|
||||||
odd = !odd;
|
odd = !odd;
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push(point(operation.data[0], operation.data[1]));
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
}
|
}
|
||||||
} else if (operation.op === "bcurveTo") {
|
} else if (operation.op === "bcurveTo") {
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push(point(operation.data[0], operation.data[1]));
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
points.push(point(operation.data[2], operation.data[3]));
|
points.push(pointFrom(operation.data[2], operation.data[3]));
|
||||||
points.push(point(operation.data[4], operation.data[5]));
|
points.push(pointFrom(operation.data[4], operation.data[5]));
|
||||||
}
|
}
|
||||||
} else if (operation.op === "lineTo") {
|
} else if (operation.op === "lineTo") {
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push(point(operation.data[0], operation.data[1]));
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,27 +364,27 @@ export const segmentIntersectRectangleElement = <
|
|||||||
element.x + element.width + gap,
|
element.x + element.width + gap,
|
||||||
element.y + element.height + gap,
|
element.y + element.height + gap,
|
||||||
];
|
];
|
||||||
const center = point(
|
const center = pointFrom(
|
||||||
(bounds[0] + bounds[2]) / 2,
|
(bounds[0] + bounds[2]) / 2,
|
||||||
(bounds[1] + bounds[3]) / 2,
|
(bounds[1] + bounds[3]) / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||||
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||||
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||||
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||||
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
|
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.map((s) => segmentsIntersectAt(segment, s))
|
.map((s) => segmentsIntersectAt(segment, s))
|
||||||
@@ -404,7 +404,7 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
);
|
);
|
||||||
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||||
pointFromVector(translatedPoint),
|
pointFromVector(translatedPoint),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
-angle as Radians,
|
-angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -442,7 +442,10 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
b * ty * Math.sign(rotatedPointY),
|
b * ty * Math.sign(rotatedPointY),
|
||||||
];
|
];
|
||||||
|
|
||||||
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
|
return pointDistance(
|
||||||
|
pointFrom(rotatedPointX, rotatedPointY),
|
||||||
|
pointFrom(minX, minY),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||||
@@ -464,7 +467,7 @@ export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
);
|
);
|
||||||
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||||
pointFromVector(translatedPoint),
|
pointFromVector(translatedPoint),
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
-angle as Radians,
|
-angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { arrayToMap } from "../excalidraw/utils";
|
|||||||
import type { LocalPoint } from "../math";
|
import type { LocalPoint } from "../math";
|
||||||
import {
|
import {
|
||||||
rangeIncludesValue,
|
rangeIncludesValue,
|
||||||
point,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
@@ -41,17 +41,17 @@ const getNonLinearElementRelativePoints = (
|
|||||||
] => {
|
] => {
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
return [
|
return [
|
||||||
point(element.width / 2, 0),
|
pointFrom(element.width / 2, 0),
|
||||||
point(element.width, element.height / 2),
|
pointFrom(element.width, element.height / 2),
|
||||||
point(element.width / 2, element.height),
|
pointFrom(element.width / 2, element.height),
|
||||||
point(0, element.height / 2),
|
pointFrom(0, element.height / 2),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
point(0, 0),
|
pointFrom(0, 0),
|
||||||
point(0 + element.width, 0),
|
pointFrom(0 + element.width, 0),
|
||||||
point(0 + element.width, element.height),
|
pointFrom(0 + element.width, element.height),
|
||||||
point(0, element.height),
|
pointFrom(0, element.height),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ const getRotatedBBox = (element: Element): Bounds => {
|
|||||||
const points = getElementRelativePoints(element);
|
const points = getElementRelativePoints(element);
|
||||||
|
|
||||||
const { cx, cy } = getMinMaxPoints(points);
|
const { cx, cy } = getMinMaxPoints(points);
|
||||||
const centerPoint = point<LocalPoint>(cx, cy);
|
const centerPoint = pointFrom<LocalPoint>(cx, cy);
|
||||||
|
|
||||||
const rotatedPoints = points.map((p) =>
|
const rotatedPoints = points.map((p) =>
|
||||||
pointRotateRads(p, centerPoint, element.angle),
|
pointRotateRads(p, centerPoint, element.angle),
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
diff --git a/node_modules/mathjax-full/js/input/asciimath/mathjax2/input/AsciiMath.js b/node_modules/mathjax-full/js/input/asciimath/mathjax2/input/AsciiMath.js
|
||||||
|
index 41f6a1f..25096c6 100644
|
||||||
|
--- a/node_modules/mathjax-full/js/input/asciimath/mathjax2/input/AsciiMath.js
|
||||||
|
+++ b/node_modules/mathjax-full/js/input/asciimath/mathjax2/input/AsciiMath.js
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-MathJax = Object.assign(global.MathJax || {}, require("../legacy/MathJax.js").MathJax);
|
||||||
|
+window.MathJax = Object.assign(window.MathJax || {}, require("../legacy/MathJax.js").MathJax);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Load component-based configuration, if any
|
||||||
|
@@ -13,11 +13,13 @@ MathJax.Ajax.Preloading(
|
||||||
|
"[MathJax]/jax/element/mml/jax.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
-require("../legacy/jax/element/mml/jax.js");
|
||||||
|
-require("../legacy/jax/input/AsciiMath/config.js");
|
||||||
|
-require("../legacy/jax/input/AsciiMath/jax.js");
|
||||||
|
+exports.LegacyAsciiMath = void 0;
|
||||||
|
+(async () => {
|
||||||
|
+ await import("../legacy/jax/element/mml/jax.js");
|
||||||
|
+ await import("../legacy/jax/input/AsciiMath/config.js");
|
||||||
|
+ await import("../legacy/jax/input/AsciiMath/jax.js");
|
||||||
|
|
||||||
|
-require("../legacy/jax/element/MmlNode.js");
|
||||||
|
+ await import("../legacy/jax/element/MmlNode.js");
|
||||||
|
|
||||||
|
var MmlFactory = require("../../../../core/MmlTree/MmlFactory.js").MmlFactory;
|
||||||
|
var factory = new MmlFactory();
|
||||||
|
@@ -37,3 +38,4 @@ exports.LegacyAsciiMath = {
|
||||||
|
return this.Compile(am,display);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
+})();
|
||||||
|
diff --git a/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax.js b/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax.js
|
||||||
|
index 903ede2..504ae4f 100644
|
||||||
|
--- a/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax.js
|
||||||
|
+++ b/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax.js
|
||||||
|
@@ -19,7 +19,7 @@ exports.MathJax = MathJax;
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
var CONSTRUCTOR = function () {
|
||||||
|
- return function () {return arguments.callee.Init.call(this,arguments)};
|
||||||
|
+ return function fn() {return fn.Init.call(this,Object.assign(arguments,{call:fn}))};
|
||||||
|
};
|
||||||
|
|
||||||
|
BASE.Object = OBJECT({
|
||||||
|
@@ -40,7 +40,7 @@ exports.MathJax = MathJax;
|
||||||
|
Init: function (args) {
|
||||||
|
var obj = this;
|
||||||
|
if (args.length === 1 && args[0] === PROTO) {return obj}
|
||||||
|
- if (!(obj instanceof args.callee)) {obj = new args.callee(PROTO)}
|
||||||
|
+ if (!(obj instanceof args.call)) {obj = new args.call(PROTO)}
|
||||||
|
return obj.Init.apply(obj,args) || obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ exports.MathJax = MathJax;
|
||||||
|
|
||||||
|
prototype: {
|
||||||
|
Init: function () {},
|
||||||
|
- SUPER: function (fn) {return fn.callee.SUPER},
|
||||||
|
+ SUPER: function (fn) {return fn.SUPER},
|
||||||
|
can: function (method) {return typeof(this[method]) === "function"},
|
||||||
|
has: function (property) {return typeof(this[property]) !== "undefined"},
|
||||||
|
isa: function (obj) {return (obj instanceof Object) && (this instanceof obj)}
|
||||||
|
@@ -177,7 +177,7 @@ exports.MathJax = MathJax;
|
||||||
|
// Create a callback from an associative array
|
||||||
|
//
|
||||||
|
var CALLBACK = function (data) {
|
||||||
|
- var cb = function () {return arguments.callee.execute.apply(arguments.callee,arguments)};
|
||||||
|
+ var cb = function fn() {return fn.execute.apply(fn,arguments)};
|
||||||
|
for (var id in CALLBACK.prototype) {
|
||||||
|
if (CALLBACK.prototype.hasOwnProperty(id)) {
|
||||||
|
if (typeof(data[id]) !== 'undefined') {cb[id] = data[id]}
|
||||||
|
diff --git a/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/jax/element/mml/jax.js b/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/jax/element/mml/jax.js
|
||||||
|
index 96fb918..473aca1 100644
|
||||||
|
--- a/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/jax/element/mml/jax.js
|
||||||
|
+++ b/node_modules/mathjax-full/js/input/asciimath/mathjax2/legacy/jax/element/mml/jax.js
|
||||||
|
@@ -813,9 +813,9 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
if (!(this.isEmbellished()) || typeof(this.core) === "undefined") {return this}
|
||||||
|
return this.data[this.core].CoreMO();
|
||||||
|
},
|
||||||
|
- toString: function () {
|
||||||
|
+ toString: function fn() {
|
||||||
|
if (this.inferred) {return '[' + this.data.join(',') + ']'}
|
||||||
|
- return this.SUPER(arguments).toString.call(this);
|
||||||
|
+ return this.SUPER(fn).toString.call(this);
|
||||||
|
},
|
||||||
|
setTeXclass: function (prev) {
|
||||||
|
var i, m = this.data.length;
|
||||||
|
@@ -1196,12 +1196,12 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
}
|
||||||
|
},
|
||||||
|
linebreakContainer: true,
|
||||||
|
- Append: function () {
|
||||||
|
+ Append: function fn() {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
|
if (!((arguments[i] instanceof MML.mtr) ||
|
||||||
|
(arguments[i] instanceof MML.mlabeledtr))) {arguments[i] = MML.mtr(arguments[i])}
|
||||||
|
}
|
||||||
|
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||||
|
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||||
|
},
|
||||||
|
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||||
|
});
|
||||||
|
@@ -1221,11 +1221,11 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
mtable: {rowalign: true, columnalign: true, groupalign: true}
|
||||||
|
},
|
||||||
|
linebreakContainer: true,
|
||||||
|
- Append: function () {
|
||||||
|
+ Append: function fn() {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
|
if (!(arguments[i] instanceof MML.mtd)) {arguments[i] = MML.mtd(arguments[i])}
|
||||||
|
}
|
||||||
|
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||||
|
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||||
|
},
|
||||||
|
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||||
|
});
|
||||||
|
@@ -1420,9 +1420,9 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
|
||||||
|
MML.xml = MML.mbase.Subclass({
|
||||||
|
type: "xml",
|
||||||
|
- Init: function () {
|
||||||
|
+ Init: function fn() {
|
||||||
|
this.div = document.createElement("div");
|
||||||
|
- return this.SUPER(arguments).Init.apply(this,arguments);
|
||||||
|
+ return this.SUPER(fn).Init.apply(this,arguments);
|
||||||
|
},
|
||||||
|
Append: function () {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
@@ -3786,6 +3786,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||||
|
|
||||||
|
"@yarnpkg/lockfile@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||||
|
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
||||||
|
|
||||||
abab@^2.0.6:
|
abab@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||||
@@ -4585,7 +4590,7 @@ chrome-trace-event@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
||||||
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
||||||
|
|
||||||
ci-info@^3.2.0:
|
ci-info@^3.2.0, ci-info@^3.7.0:
|
||||||
version "3.9.0"
|
version "3.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
|
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
|
||||||
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
|
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
|
||||||
@@ -4694,6 +4699,11 @@ commander@7, commander@^7.0.0, commander@^7.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||||
|
|
||||||
|
commander@9.2.0:
|
||||||
|
version "9.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9"
|
||||||
|
integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==
|
||||||
|
|
||||||
commander@^2.20.0:
|
commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
@@ -6060,6 +6070,11 @@ eslint@^7.32.0:
|
|||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
v8-compile-cache "^2.0.3"
|
v8-compile-cache "^2.0.3"
|
||||||
|
|
||||||
|
esm@^3.2.25:
|
||||||
|
version "3.2.25"
|
||||||
|
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
|
||||||
|
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
|
||||||
|
|
||||||
espree@^7.3.0, espree@^7.3.1:
|
espree@^7.3.0, espree@^7.3.1:
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
|
||||||
@@ -6313,6 +6328,13 @@ find-up@^4.0.0:
|
|||||||
locate-path "^5.0.0"
|
locate-path "^5.0.0"
|
||||||
path-exists "^4.0.0"
|
path-exists "^4.0.0"
|
||||||
|
|
||||||
|
find-yarn-workspace-root@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
||||||
|
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
|
||||||
|
dependencies:
|
||||||
|
micromatch "^4.0.2"
|
||||||
|
|
||||||
firebase@8.3.3:
|
firebase@8.3.3:
|
||||||
version "8.3.3"
|
version "8.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.3.3.tgz#21d8fb8eec2c43b0d8f98ab6bda5535b7454fa54"
|
resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.3.3.tgz#21d8fb8eec2c43b0d8f98ab6bda5535b7454fa54"
|
||||||
@@ -6428,7 +6450,7 @@ fs-extra@^11.1.0:
|
|||||||
jsonfile "^6.0.1"
|
jsonfile "^6.0.1"
|
||||||
universalify "^2.0.0"
|
universalify "^2.0.0"
|
||||||
|
|
||||||
fs-extra@^9.0.1:
|
fs-extra@^9.0.0, fs-extra@^9.0.1:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
||||||
@@ -6625,7 +6647,7 @@ gopd@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
@@ -7018,6 +7040,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag "^1.0.0"
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
|
is-docker@^2.0.0:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||||
|
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
is-extglob@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
@@ -7174,6 +7201,13 @@ is-weakset@^2.0.3:
|
|||||||
call-bind "^1.0.7"
|
call-bind "^1.0.7"
|
||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
|
|
||||||
|
is-wsl@^2.1.1:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||||
|
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||||
|
dependencies:
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
|
||||||
isarray@^2.0.5:
|
isarray@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||||
@@ -7438,6 +7472,16 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||||
|
|
||||||
|
json-stable-stringify@^1.0.2:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454"
|
||||||
|
integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.5"
|
||||||
|
isarray "^2.0.5"
|
||||||
|
jsonify "^0.0.1"
|
||||||
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
json5@^1.0.2:
|
json5@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||||
@@ -7459,6 +7503,11 @@ jsonfile@^6.0.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
jsonify@^0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
|
||||||
|
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
|
||||||
|
|
||||||
jsonpointer@^5.0.0:
|
jsonpointer@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||||
@@ -7498,6 +7547,13 @@ kind-of@^6.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||||
|
|
||||||
|
klaw-sync@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
|
||||||
|
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.11"
|
||||||
|
|
||||||
kleur@^4.0.3:
|
kleur@^4.0.3:
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||||
@@ -7778,6 +7834,16 @@ make-dir@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^7.5.3"
|
semver "^7.5.3"
|
||||||
|
|
||||||
|
mathjax-full@3.2.2:
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.2.tgz#43f02e55219db393030985d2b6537ceae82f1fa7"
|
||||||
|
integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==
|
||||||
|
dependencies:
|
||||||
|
esm "^3.2.25"
|
||||||
|
mhchemparser "^4.1.0"
|
||||||
|
mj-context-menu "^0.6.1"
|
||||||
|
speech-rule-engine "^4.0.6"
|
||||||
|
|
||||||
mdast-util-from-markdown@^1.3.0:
|
mdast-util-from-markdown@^1.3.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0"
|
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0"
|
||||||
@@ -7839,6 +7905,11 @@ mermaid@10.9.0:
|
|||||||
uuid "^9.0.0"
|
uuid "^9.0.0"
|
||||||
web-worker "^1.2.0"
|
web-worker "^1.2.0"
|
||||||
|
|
||||||
|
mhchemparser@^4.1.0:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.2.1.tgz#d73982e66bc06170a85b1985600ee9dabe157cb0"
|
||||||
|
integrity sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==
|
||||||
|
|
||||||
micromark-core-commonmark@^1.0.1:
|
micromark-core-commonmark@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
|
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
|
||||||
@@ -8041,6 +8112,14 @@ micromatch@^4.0.0, micromatch@^4.0.4:
|
|||||||
braces "^3.0.3"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
|
micromatch@^4.0.2:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||||
|
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||||
|
dependencies:
|
||||||
|
braces "^3.0.2"
|
||||||
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
mime-db@1.52.0:
|
mime-db@1.52.0:
|
||||||
version "1.52.0"
|
version "1.52.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
@@ -8111,6 +8190,11 @@ minimist@^1.2.0, minimist@^1.2.6:
|
|||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||||
|
|
||||||
|
mj-context-menu@^0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69"
|
||||||
|
integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==
|
||||||
|
|
||||||
mkdirp-classic@^0.5.2:
|
mkdirp-classic@^0.5.2:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||||
@@ -8404,6 +8488,14 @@ open-color@1.9.1:
|
|||||||
resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35"
|
resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35"
|
||||||
integrity sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==
|
integrity sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==
|
||||||
|
|
||||||
|
open@^7.4.2:
|
||||||
|
version "7.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||||
|
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
||||||
|
dependencies:
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
is-wsl "^2.1.1"
|
||||||
|
|
||||||
opener@^1.5.1, opener@^1.5.2:
|
opener@^1.5.1, opener@^1.5.2:
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||||
@@ -8421,6 +8513,11 @@ optionator@^0.9.1:
|
|||||||
type-check "^0.4.0"
|
type-check "^0.4.0"
|
||||||
word-wrap "^1.2.5"
|
word-wrap "^1.2.5"
|
||||||
|
|
||||||
|
os-tmpdir@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
|
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
||||||
|
|
||||||
p-limit@^2.2.0:
|
p-limit@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||||
@@ -8497,6 +8594,27 @@ pascal-case@^3.1.2:
|
|||||||
no-case "^3.0.4"
|
no-case "^3.0.4"
|
||||||
tslib "^2.0.3"
|
tslib "^2.0.3"
|
||||||
|
|
||||||
|
patch-package@8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
|
||||||
|
integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
|
||||||
|
dependencies:
|
||||||
|
"@yarnpkg/lockfile" "^1.1.0"
|
||||||
|
chalk "^4.1.2"
|
||||||
|
ci-info "^3.7.0"
|
||||||
|
cross-spawn "^7.0.3"
|
||||||
|
find-yarn-workspace-root "^2.0.0"
|
||||||
|
fs-extra "^9.0.0"
|
||||||
|
json-stable-stringify "^1.0.2"
|
||||||
|
klaw-sync "^6.0.0"
|
||||||
|
minimist "^1.2.6"
|
||||||
|
open "^7.4.2"
|
||||||
|
rimraf "^2.6.3"
|
||||||
|
semver "^7.5.3"
|
||||||
|
slash "^2.0.0"
|
||||||
|
tmp "^0.0.33"
|
||||||
|
yaml "^2.2.2"
|
||||||
|
|
||||||
path-data-parser@0.1.0, path-data-parser@^0.1.0:
|
path-data-parser@0.1.0, path-data-parser@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c"
|
resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c"
|
||||||
@@ -8733,6 +8851,11 @@ postcss@^8.4.32, postcss@^8.4.38, postcss@^8.4.7:
|
|||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.2.0"
|
source-map-js "^1.2.0"
|
||||||
|
|
||||||
|
postinstall-postinstall@2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3"
|
||||||
|
integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==
|
||||||
|
|
||||||
prelude-ls@^1.2.1:
|
prelude-ls@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
@@ -9171,6 +9294,13 @@ rimraf@3.0.2, rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
rimraf@^2.6.3:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
robust-predicates@^3.0.2:
|
robust-predicates@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||||
@@ -9481,6 +9611,11 @@ size-limit@9.0.0:
|
|||||||
nanospinner "^1.1.0"
|
nanospinner "^1.1.0"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
|
slash@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||||
|
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
||||||
|
|
||||||
slash@^3.0.0:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
@@ -9596,6 +9731,15 @@ sourcemap-codec@^1.4.8:
|
|||||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||||
|
|
||||||
|
speech-rule-engine@^4.0.6:
|
||||||
|
version "4.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz#b655dacbad3dae04acc0f7665e26ef258397dd09"
|
||||||
|
integrity sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==
|
||||||
|
dependencies:
|
||||||
|
commander "9.2.0"
|
||||||
|
wicked-good-xpath "1.3.0"
|
||||||
|
xmldom-sre "0.1.31"
|
||||||
|
|
||||||
sprintf-js@~1.0.2:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
@@ -9633,7 +9777,7 @@ string-natural-compare@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -9651,6 +9795,15 @@ string-width@^4.1.0, string-width@^4.2.0:
|
|||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
string-width@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
|
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||||
@@ -9722,7 +9875,14 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -9941,6 +10101,13 @@ tinyspy@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.0.tgz#cb61644f2713cd84dee184863f4642e06ddf0585"
|
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.0.tgz#cb61644f2713cd84dee184863f4642e06ddf0585"
|
||||||
integrity sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==
|
integrity sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==
|
||||||
|
|
||||||
|
tmp@^0.0.33:
|
||||||
|
version "0.0.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||||
|
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||||
|
dependencies:
|
||||||
|
os-tmpdir "~1.0.2"
|
||||||
|
|
||||||
to-fast-properties@^1.0.3:
|
to-fast-properties@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
|
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
|
||||||
@@ -10786,6 +10953,11 @@ why-is-node-running@^2.3.0:
|
|||||||
siginfo "^2.0.0"
|
siginfo "^2.0.0"
|
||||||
stackback "0.0.2"
|
stackback "0.0.2"
|
||||||
|
|
||||||
|
wicked-good-xpath@1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c"
|
||||||
|
integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==
|
||||||
|
|
||||||
wildcard@^2.0.0:
|
wildcard@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
|
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
|
||||||
@@ -10954,7 +11126,7 @@ workbox-window@7.1.0, workbox-window@^7.0.0:
|
|||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
workbox-core "7.1.0"
|
workbox-core "7.1.0"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@@ -10972,6 +11144,15 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
@@ -11016,6 +11197,11 @@ xmlchars@^2.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||||
|
|
||||||
|
xmldom-sre@0.1.31:
|
||||||
|
version "0.1.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
|
||||||
|
integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
|
||||||
|
|
||||||
xmlhttprequest-ssl@~2.0.0:
|
xmlhttprequest-ssl@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
||||||
@@ -11046,6 +11232,11 @@ yaml@^1.10.0, yaml@^1.10.2:
|
|||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||||
|
|
||||||
|
yaml@^2.2.2:
|
||||||
|
version "2.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
|
||||||
|
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
yargs-parser@^21.1.1:
|
||||||
version "21.1.1"
|
version "21.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
|
|||||||
Reference in New Issue
Block a user