Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 521896cccf | |||
| fe318126bd |
@@ -4,7 +4,6 @@ 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,
|
||||||
@@ -127,6 +126,8 @@ import DebugCanvas, {
|
|||||||
loadSavedDebugState,
|
loadSavedDebugState,
|
||||||
} from "./components/DebugCanvas";
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
|
import type { SaveWarningRef } from "./components/SaveWarning";
|
||||||
|
import { SaveWarning } from "./components/SaveWarning";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -332,6 +333,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const [langCode, setLangCode] = useAppLangCode();
|
const [langCode, setLangCode] = useAppLangCode();
|
||||||
|
|
||||||
|
const activityRef = useRef<SaveWarningRef | null>(null);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -356,8 +359,6 @@ 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, () => {
|
||||||
@@ -618,6 +619,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
collabAPI.syncElements(elements);
|
collabAPI.syncElements(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activityRef.current?.activity();
|
||||||
|
|
||||||
// this check is redundant, but since this is a hot path, it's best
|
// this check is redundant, but since this is a hot path, it's best
|
||||||
// not to evaludate the nested expression every time
|
// not to evaludate the nested expression every time
|
||||||
if (!LocalData.isSavePaused()) {
|
if (!LocalData.isSavePaused()) {
|
||||||
@@ -859,6 +862,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
setTheme={(theme) => setAppTheme(theme)}
|
setTheme={(theme) => setAppTheme(theme)}
|
||||||
refresh={() => forceRefresh((prev) => !prev)}
|
refresh={() => forceRefresh((prev) => !prev)}
|
||||||
/>
|
/>
|
||||||
|
<SaveWarning ref={activityRef} />
|
||||||
<AppWelcomeScreen
|
<AppWelcomeScreen
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
onCollabDialogOpen={onCollabDialogOpen}
|
||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
|
import { getShortcutKey } from "../../packages/excalidraw/utils";
|
||||||
|
|
||||||
|
export type SaveWarningRef = {
|
||||||
|
activity: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaveWarning = forwardRef<SaveWarningRef, {}>((props, ref) => {
|
||||||
|
const dialogRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
/**
|
||||||
|
* Call this API method via the ref to hide warning message
|
||||||
|
* and start an idle timer again.
|
||||||
|
*/
|
||||||
|
activity: async () => {
|
||||||
|
if (timerRef.current != null) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
dialogRef.current?.classList.remove("animate");
|
||||||
|
}
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
timerRef.current = null;
|
||||||
|
dialogRef.current?.classList.add("animate");
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dialogRef} className="alert-save">
|
||||||
|
<div className="dialog">
|
||||||
|
{t("alerts.saveYourContent", {
|
||||||
|
shortcut: getShortcutKey("CtrlOrCmd + S"),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -130,6 +130,15 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Register Assistant as the UI font, before the scene inits -->
|
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|||||||
@@ -18,6 +18,43 @@
|
|||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-save {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 10vh;
|
||||||
|
margin-inline: auto;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0s;
|
||||||
|
|
||||||
|
&.animate {
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
margin-inline: 10px;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
|
||||||
|
resize: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-text-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.encrypted-icon {
|
.encrypted-icon {
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
|
||||||
assetsInlineLimit: 0,
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
woff2BrowserPlugin(),
|
woff2BrowserPlugin(),
|
||||||
|
|||||||
@@ -32,9 +32,7 @@
|
|||||||
"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",
|
||||||
@@ -63,7 +61,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517)
|
|
||||||
|
|
||||||
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
||||||
|
|
||||||
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
||||||
@@ -305,7 +303,6 @@ 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,
|
||||||
measureTextElement,
|
measureText,
|
||||||
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 } from "../utils";
|
import { arrayToMap, getFontString } 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,9 +51,11 @@ 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 } = measureTextElement(boundTextElement, {
|
const { width, height } = measureText(
|
||||||
text: boundTextElement.originalText,
|
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 { pointFrom } from "../../math";
|
import { point } 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
|
||||||
? pointFrom(firstPoint[0], firstPoint[1])
|
? point(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 { pointFrom } from "../../math";
|
import { point } 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: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(0, -35),
|
point(0, -35),
|
||||||
pointFrom(-90.9, -35),
|
point(-90.9, -35),
|
||||||
pointFrom(-90.9, 204.9),
|
point(-90.9, 204.9),
|
||||||
pointFrom(65.1, 204.9),
|
point(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 { pointFrom, vector } from "../../math";
|
import { point, 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 =>
|
||||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
point(p[0] - newElement.x, p[1] - newElement.y),
|
||||||
),
|
),
|
||||||
vector(0, 0),
|
vector(0, 0),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
ActionPredicateFn,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -46,7 +45,6 @@ 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;
|
||||||
|
|
||||||
@@ -74,37 +72,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -121,7 +88,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]
|
||||||
: this.isActionEnabled(action, { noPredicates: true })) &&
|
: true) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -180,7 +147,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]
|
||||||
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
: true)
|
||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@@ -202,7 +169,6 @@ 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}
|
||||||
@@ -216,31 +182,13 @@ export class ActionManager {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (
|
isActionEnabled = (action: Action) => {
|
||||||
action: Action,
|
const elements = this.getElementsIncludingDeleted();
|
||||||
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;
|
|
||||||
|
|
||||||
if (
|
return (
|
||||||
!opts?.noPredicates &&
|
!action.predicate ||
|
||||||
action.predicate &&
|
action.predicate(elements, appState, this.app.props, this.app)
|
||||||
!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,12 +2,11 @@ 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, CustomActionName } from "./types";
|
import type { ActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName =
|
||||||
| SubtypeOf<
|
| SubtypeOf<
|
||||||
ActionName,
|
ActionName,
|
||||||
| CustomActionName
|
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "clearCanvas"
|
| "clearCanvas"
|
||||||
@@ -55,15 +54,6 @@ 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,24 +41,10 @@ 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"
|
||||||
@@ -193,7 +179,6 @@ 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,8 +170,6 @@ 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 { pointFrom } from "../math";
|
import { point } from "../math";
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
DEFAULT_CHART_COLOR_INDEX,
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
@@ -13,8 +13,6 @@ 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[];
|
||||||
|
|
||||||
@@ -27,8 +25,6 @@ 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";
|
||||||
@@ -199,17 +195,13 @@ 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:
|
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||||
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,
|
||||||
@@ -217,7 +209,6 @@ const chartXLabels = (
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
...custom,
|
|
||||||
});
|
});
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
@@ -238,7 +229,6 @@ 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({
|
||||||
@@ -249,7 +239,6 @@ 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];
|
||||||
@@ -271,8 +260,7 @@ const chartLines = (
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
points: [point(0, 0), point(chartWidth, 0)],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
const yLine = newLinearElement({
|
||||||
@@ -283,8 +271,7 @@ const chartLines = (
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
|
points: [point(0, 0), point(0, -chartHeight)],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
const maxLine = newLinearElement({
|
||||||
@@ -297,8 +284,7 @@ const chartLines = (
|
|||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
points: [point(0, 0), point(chartWidth, 0)],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
return [xLine, yLine, maxLine];
|
||||||
@@ -325,7 +311,6 @@ 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;
|
||||||
|
|
||||||
@@ -342,7 +327,6 @@ const chartBaseElements = (
|
|||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
...selectSubtype(spreadsheet, "rectangle"),
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -375,7 +359,6 @@ 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"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -426,7 +409,6 @@ 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) => {
|
||||||
@@ -443,7 +425,6 @@ 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"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -460,8 +441,7 @@ const chartTypeLine = (
|
|||||||
height: cy,
|
height: cy,
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [pointFrom(0, 0), pointFrom(0, cy)],
|
points: [point(0, 0), point(0, cy)],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import type { AppState, BinaryFiles } from "./types";
|
import type { 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,7 +333,6 @@ 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);
|
||||||
|
|
||||||
@@ -350,10 +349,6 @@ 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,7 +21,6 @@ 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 {
|
||||||
@@ -137,7 +136,6 @@ 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,18 +301,6 @@ 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,
|
||||||
@@ -457,7 +445,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 { pointFrom, pointDistance, vector } from "../../math";
|
import { point, 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!);
|
||||||
@@ -722,7 +710,6 @@ 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,
|
||||||
@@ -759,19 +746,6 @@ 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) {
|
||||||
@@ -2977,7 +2951,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, this.state);
|
const data = await parseClipboard(event, isPlainPaste);
|
||||||
if (!file && !isPlainPaste) {
|
if (!file && !isPlainPaste) {
|
||||||
if (data.mixedContent) {
|
if (data.mixedContent) {
|
||||||
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
||||||
@@ -3415,7 +3389,6 @@ 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({
|
||||||
@@ -4937,7 +4910,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.getElementHitThreshold(),
|
this.getElementHitThreshold(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isPointInShape(pointFrom(x, y), selectionShape);
|
return isPointInShape(point(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
|
||||||
@@ -5125,7 +5098,6 @@ 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,
|
||||||
@@ -5297,7 +5269,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
point(scenePointer.x, scenePointer.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -5309,14 +5281,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isTouchScreen: boolean,
|
isTouchScreen: boolean,
|
||||||
) => {
|
) => {
|
||||||
const draggedDistance = pointDistance(
|
const draggedDistance = pointDistance(
|
||||||
pointFrom(
|
point(
|
||||||
this.lastPointerDownEvent!.clientX,
|
this.lastPointerDownEvent!.clientX,
|
||||||
this.lastPointerDownEvent!.clientY,
|
this.lastPointerDownEvent!.clientY,
|
||||||
),
|
),
|
||||||
pointFrom(
|
point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY),
|
||||||
this.lastPointerUpEvent!.clientX,
|
|
||||||
this.lastPointerUpEvent!.clientY,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!this.hitLinkElement ||
|
!this.hitLinkElement ||
|
||||||
@@ -5335,7 +5304,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
point(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
@@ -5346,7 +5315,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
point(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
@@ -5596,7 +5565,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// threshold, add a point
|
// threshold, add a point
|
||||||
if (
|
if (
|
||||||
pointDistance(
|
pointDistance(
|
||||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
point(scenePointerX - rx, scenePointerY - ry),
|
||||||
lastPoint,
|
lastPoint,
|
||||||
) >= LINE_CONFIRM_THRESHOLD
|
) >= LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -5605,7 +5574,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
...points,
|
...points,
|
||||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
point<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -5619,7 +5588,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
points.length > 2 &&
|
points.length > 2 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
pointDistance(
|
pointDistance(
|
||||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
point(scenePointerX - rx, scenePointerY - ry),
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -5667,7 +5636,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
[
|
[
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
pointFrom<LocalPoint>(
|
point<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
),
|
),
|
||||||
@@ -5686,7 +5655,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
pointFrom<LocalPoint>(
|
point<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
),
|
),
|
||||||
@@ -5915,8 +5884,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
point(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
const threshold = this.getElementHitThreshold();
|
const threshold = this.getElementHitThreshold();
|
||||||
const p = { ...pointerDownState.lastCoords };
|
const p = { ...pointerDownState.lastCoords };
|
||||||
@@ -6428,7 +6397,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
point(scenePointer.x, scenePointer.y),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||||
@@ -7119,7 +7088,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: [pointFrom<LocalPoint>(0, 0)],
|
points: [point<LocalPoint>(0, 0)],
|
||||||
pressures: simulatePressure ? [] : [event.pressure],
|
pressures: simulatePressure ? [] : [event.pressure],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7282,7 +7251,6 @@ 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,
|
||||||
});
|
});
|
||||||
@@ -7329,10 +7297,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
multiElement.points.length > 1 &&
|
multiElement.points.length > 1 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
pointDistance(
|
pointDistance(
|
||||||
pointFrom(
|
point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry),
|
||||||
pointerDownState.origin.x - rx,
|
|
||||||
pointerDownState.origin.y - ry,
|
|
||||||
),
|
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -7399,7 +7364,6 @@ 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,
|
||||||
@@ -7419,7 +7383,6 @@ 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,
|
||||||
});
|
});
|
||||||
@@ -7436,7 +7399,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
points: [...element.points, point<LocalPoint>(0, 0)],
|
||||||
});
|
});
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
@@ -7500,7 +7463,6 @@ 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;
|
||||||
@@ -7690,8 +7652,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
pointDistance(
|
pointDistance(
|
||||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
point(pointerCoords.x, pointerCoords.y),
|
||||||
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
|
point(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||||
) < DRAGGING_THRESHOLD
|
) < DRAGGING_THRESHOLD
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -8040,7 +8002,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -8069,7 +8031,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -8077,7 +8039,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElbowArrow(
|
mutateElbowArrow(
|
||||||
newElement,
|
newElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
[...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||||
vector(0, 0),
|
vector(0, 0),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -8089,7 +8051,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -8398,9 +8360,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: [...newElement.pressures, childEvent.pressure];
|
: [...newElement.pressures, childEvent.pressure];
|
||||||
|
|
||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
lastCommittedPoint: point<LocalPoint>(dx, dy),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -8447,7 +8409,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [
|
points: [
|
||||||
...newElement.points,
|
...newElement.points,
|
||||||
pointFrom<LocalPoint>(
|
point<LocalPoint>(
|
||||||
pointerCoords.x - newElement.x,
|
pointerCoords.x - newElement.x,
|
||||||
pointerCoords.y - newElement.y,
|
pointerCoords.y - newElement.y,
|
||||||
),
|
),
|
||||||
@@ -8761,8 +8723,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.eraserTrail.endPath();
|
this.eraserTrail.endPath();
|
||||||
|
|
||||||
const draggedDistance = pointDistance(
|
const draggedDistance = pointDistance(
|
||||||
pointFrom(pointerStart.clientX, pointerStart.clientY),
|
point(pointerStart.clientX, pointerStart.clientY),
|
||||||
pointFrom(pointerEnd.clientX, pointerEnd.clientY),
|
point(pointerEnd.clientX, pointerEnd.clientY),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (draggedDistance === 0) {
|
if (draggedDistance === 0) {
|
||||||
@@ -10080,39 +10042,6 @@ 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,
|
||||||
@@ -10133,29 +10062,6 @@ 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,7 +56,6 @@ 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";
|
||||||
@@ -300,7 +299,6 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<SubtypeToggles />
|
|
||||||
{isCollaborating && (
|
{isCollaborating && (
|
||||||
<Island
|
<Island
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ 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;
|
||||||
@@ -90,7 +89,6 @@ 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, ElementsMap } from "../element/types";
|
import type { ChartType } 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,12 +11,6 @@ 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;
|
||||||
|
|
||||||
@@ -32,64 +26,41 @@ 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 () => {
|
||||||
(async () => {
|
svg = await exportToSvg(
|
||||||
let elements: ChartElements;
|
elements,
|
||||||
await ensureSubtypesLoaded(
|
{
|
||||||
props.spreadsheet?.activeSubtypes ?? [],
|
exportBackground: false,
|
||||||
() => {
|
viewBackgroundColor: oc.white,
|
||||||
if (!props.spreadsheet) {
|
},
|
||||||
return;
|
null, // files
|
||||||
}
|
);
|
||||||
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
elements = renderSpreadsheet(
|
if (props.selected) {
|
||||||
props.chartType,
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
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 { pointFrom, type GlobalPoint } from "../../../math";
|
import { point, 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,
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
@@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextHeight,
|
nextHeight,
|
||||||
initialHeight,
|
initialHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
pointFrom(x1, y1),
|
point(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 { pointFrom, pointRotateRads } from "../../../math";
|
import { point, 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(
|
||||||
pointFrom(origElement.x, origElement.y),
|
point(origElement.x, origElement.y),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ const moveGroupTo = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(latestElement.x, latestElement.y),
|
point(latestElement.x, latestElement.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(origElement.x, origElement.y),
|
point(origElement.x, origElement.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(el.x, el.y),
|
point(el.x, el.y),
|
||||||
pointFrom(cx, cy),
|
point(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 { pointFrom, pointRotateRads } from "../../../math";
|
import { point, 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(
|
||||||
pointFrom(origElement.x, origElement.y),
|
point(origElement.x, origElement.y),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ const Position = ({
|
|||||||
appState,
|
appState,
|
||||||
}: PositionProps) => {
|
}: PositionProps) => {
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(element.x, element.y),
|
point(element.x, element.y),
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
point(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, pointFrom, pointRotateRads } from "../../../math";
|
import { degreesToRadians, point, 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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(rectangle.x, rectangle.y),
|
point(rectangle.x, rectangle.y),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Radians } from "../../../math";
|
import type { Radians } from "../../../math";
|
||||||
import { pointFrom, pointRotateRads } from "../../../math";
|
import { point, 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(
|
||||||
pointFrom(originalElement.x, originalElement.y),
|
point(originalElement.x, originalElement.y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(newTopLeftX, newTopLeftY),
|
point(newTopLeftX, newTopLeftY),
|
||||||
pointFrom(cx + changeInX, cy + changeInY),
|
point(cx + changeInX, cy + changeInY),
|
||||||
-originalElement.angle as Radians,
|
-originalElement.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
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 { pointFrom, type GlobalPoint } from "../../../math";
|
import { point, 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,
|
||||||
pointFrom(event.clientX, event.clientY),
|
point(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 { pointFrom, pointRotateRads } from "../../../math";
|
import { point, 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(
|
||||||
pointFrom(x + linkWidth / 2, y + linkHeight / 2),
|
point(x + linkWidth / 2, y + linkHeight / 2),
|
||||||
pointFrom(centerX, centerY),
|
point(centerX, centerY),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
@@ -85,10 +85,5 @@ export const isPointHittingLink = (
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return isPointHittingLinkIcon(
|
return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y));
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
appState,
|
|
||||||
pointFrom(x, y),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "#d8f5a2",
|
"backgroundColor": "#d8f5a2",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id47",
|
"id": "id45",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "id48",
|
"id": "id46",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id48",
|
"id": "id46",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id49",
|
"elementId": "id47",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": -0.08139534883720931,
|
"focus": -0.08139534883720931,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id47",
|
"id": "id45",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id50",
|
"id": "id48",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id50",
|
"id": "id48",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id51",
|
"id": "id49",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id50",
|
"containerId": "id48",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id40",
|
"id": "id38",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id42",
|
"elementId": "id40",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id41",
|
"elementId": "id39",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id39",
|
"containerId": "id37",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id39",
|
"id": "id37",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id39",
|
"id": "id37",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id44",
|
"id": "id42",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"elbowed": false,
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id46",
|
"elementId": "id44",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id45",
|
"elementId": "id43",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"autoResize": true,
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id43",
|
"containerId": "id41",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id43",
|
"id": "id41",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id43",
|
"id": "id41",
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id56",
|
"id": "id54",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id57",
|
"id": "id55",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id58",
|
"id": "id56",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id59",
|
"id": "id57",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id60",
|
"id": "id58",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
"id": "id61",
|
"id": "id59",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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, pointFrom } from "../../math";
|
import { isFiniteNumber, point } from "../../math";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -121,8 +121,7 @@ const repairBinding = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||||
subtype?: ExcalidrawElement["subtype"];
|
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
@@ -185,9 +184,6 @@ 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;
|
||||||
@@ -272,7 +268,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
|
||||||
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
? [point(0, 0), point(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) {
|
||||||
@@ -300,7 +296,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
|
||||||
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
? [point(0, 0), point(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) {
|
||||||
@@ -601,12 +597,6 @@ 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 { pointFrom } from "../../math";
|
import { point } from "../../math";
|
||||||
|
|
||||||
const opts = { regenerateIds: false };
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
@@ -309,32 +309,28 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Test Frames", () => {
|
describe("Test Frames", () => {
|
||||||
const elements: ExcalidrawElementSkeleton[] = [
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
x: 10,
|
|
||||||
y: 10,
|
|
||||||
strokeWidth: 2,
|
|
||||||
id: "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "diamond",
|
|
||||||
x: 120,
|
|
||||||
y: 20,
|
|
||||||
backgroundColor: "#fff3bf",
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
text: "HELLO EXCALIDRAW",
|
|
||||||
strokeColor: "#099268",
|
|
||||||
fontSize: 30,
|
|
||||||
},
|
|
||||||
id: "2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it("should transform frames and update frame ids when regenerated", () => {
|
it("should transform frames and update frame ids when regenerated", () => {
|
||||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
...elements,
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "frame",
|
type: "frame",
|
||||||
children: ["1", "2"],
|
children: ["1", "2"],
|
||||||
@@ -356,9 +352,28 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should consider user defined frame dimensions over calculated when provided", () => {
|
it("should consider max of calculated and frame dimensions when provided", () => {
|
||||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
...elements,
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "frame",
|
type: "frame",
|
||||||
children: ["1", "2"],
|
children: ["1", "2"],
|
||||||
@@ -373,27 +388,7 @@ describe("Test Transform", () => {
|
|||||||
);
|
);
|
||||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||||
expect(frame.width).toBe(800);
|
expect(frame.width).toBe(800);
|
||||||
expect(frame.height).toBe(100);
|
expect(frame.height).toBe(126);
|
||||||
});
|
|
||||||
|
|
||||||
it("should consider user defined frame coordinates calculated when provided", () => {
|
|
||||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
|
||||||
...elements,
|
|
||||||
{
|
|
||||||
type: "frame",
|
|
||||||
children: ["1", "2"],
|
|
||||||
name: "My frame",
|
|
||||||
x: 100,
|
|
||||||
y: 300,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const excalidrawElements = convertToExcalidrawElements(
|
|
||||||
elementsSkeleton,
|
|
||||||
opts,
|
|
||||||
);
|
|
||||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
|
||||||
expect(frame.x).toBe(100);
|
|
||||||
expect(frame.y).toBe(300);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -917,7 +912,7 @@ describe("Test Transform", () => {
|
|||||||
x: 111.262,
|
x: 111.262,
|
||||||
y: 57,
|
y: 57,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [pointFrom(0, 0), pointFrom(272.985, 0)],
|
points: [point(0, 0), point(272.985, 0)],
|
||||||
label: {
|
label: {
|
||||||
text: "How are you?",
|
text: "How are you?",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
@@ -940,7 +935,7 @@ describe("Test Transform", () => {
|
|||||||
x: 77.017,
|
x: 77.017,
|
||||||
y: 79,
|
y: 79,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [pointFrom(0, 0)],
|
points: [point(0, 0)],
|
||||||
label: {
|
label: {
|
||||||
text: "Friendship",
|
text: "Friendship",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
assertNever,
|
assertNever,
|
||||||
cloneJSON,
|
cloneJSON,
|
||||||
getFontString,
|
getFontString,
|
||||||
isDevEnv,
|
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
@@ -54,7 +53,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 { pointFrom, type LocalPoint } from "../../math";
|
import { point, type LocalPoint } from "../../math";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -537,7 +536,7 @@ export const convertToExcalidrawElements = (
|
|||||||
excalidrawElement = newLinearElement({
|
excalidrawElement = newLinearElement({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
points: [point(0, 0), point(width, height)],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -550,7 +549,7 @@ export const convertToExcalidrawElements = (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
points: [point(0, 0), point(width, height)],
|
||||||
...element,
|
...element,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
});
|
});
|
||||||
@@ -718,7 +717,7 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Once all the excalidraw elements are created, we can add frames since we
|
// Once all the excalidraw elements are created, we can add frames since we
|
||||||
// need to calculate coordinates and dimensions of frame which is possible after all
|
// need to calculate coordinates and dimensions of frame which is possibe after all
|
||||||
// frame children are processed.
|
// frame children are processed.
|
||||||
for (const [id, element] of elementsWithIds) {
|
for (const [id, element] of elementsWithIds) {
|
||||||
if (element.type !== "frame" && element.type !== "magicframe") {
|
if (element.type !== "frame" && element.type !== "magicframe") {
|
||||||
@@ -765,26 +764,10 @@ export const convertToExcalidrawElements = (
|
|||||||
maxX = maxX + PADDING;
|
maxX = maxX + PADDING;
|
||||||
maxY = maxY + PADDING;
|
maxY = maxY + PADDING;
|
||||||
|
|
||||||
const frameX = frame?.x || minX;
|
// Take the max of calculated and provided frame dimensions, whichever is higher
|
||||||
const frameY = frame?.y || minY;
|
const width = Math.max(frame?.width, maxX - minX);
|
||||||
const frameWidth = frame?.width || maxX - minX;
|
const height = Math.max(frame?.height, maxY - minY);
|
||||||
const frameHeight = frame?.height || maxY - minY;
|
Object.assign(frame, { x: minX, y: minY, width, height });
|
||||||
|
|
||||||
Object.assign(frame, {
|
|
||||||
x: frameX,
|
|
||||||
y: frameY,
|
|
||||||
width: frameWidth,
|
|
||||||
height: frameHeight,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
isDevEnv() &&
|
|
||||||
element.children.length &&
|
|
||||||
(frame?.x || frame?.y || frame?.width || frame?.height)
|
|
||||||
) {
|
|
||||||
console.info(
|
|
||||||
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return elementStore.getElements();
|
return elementStore.getElements();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import {
|
|||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
point,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
@@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(
|
vectorFromPoint(
|
||||||
p,
|
p,
|
||||||
pointFrom<GlobalPoint>(
|
point<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,
|
||||||
pointFrom(p[0], p[1] - 2 * bindableElement.height),
|
point(p[0], p[1] - 2 * bindableElement.height),
|
||||||
pointFrom(p[0], p[1] + 2 * bindableElement.height),
|
point(p[0], p[1] + 2 * bindableElement.height),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
) ?? []),
|
) ?? []),
|
||||||
...(intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
pointFrom(p[0] - 2 * bindableElement.width, p[1]),
|
point(p[0] - 2 * bindableElement.width, p[1]),
|
||||||
pointFrom(p[0] + 2 * bindableElement.width, p[1]),
|
point(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(
|
||||||
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
point((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(
|
||||||
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
point(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(
|
||||||
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
point(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 = pointFrom<GlobalPoint>(
|
const center = point<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>(
|
||||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
point(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE),
|
point(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -871,16 +871,13 @@ 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(
|
||||||
pointFrom(
|
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
|
||||||
element.x,
|
|
||||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
|
||||||
),
|
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -894,7 +891,7 @@ export const avoidRectangularCorner = (
|
|||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
point(
|
||||||
element.x + element.width,
|
element.x + element.width,
|
||||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||||
),
|
),
|
||||||
@@ -903,7 +900,7 @@ export const avoidRectangularCorner = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
point(
|
||||||
element.x + element.width + FIXED_BINDING_DISTANCE,
|
element.x + element.width + FIXED_BINDING_DISTANCE,
|
||||||
element.y + element.height,
|
element.y + element.height,
|
||||||
),
|
),
|
||||||
@@ -920,16 +917,13 @@ export const avoidRectangularCorner = (
|
|||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
|
||||||
element.x + element.width,
|
|
||||||
element.y - FIXED_BINDING_DISTANCE,
|
|
||||||
),
|
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
@@ -944,10 +938,7 @@ export const snapToMid = (
|
|||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
||||||
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
|
||||||
@@ -962,7 +953,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// LEFT
|
// LEFT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
point(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -973,7 +964,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// TOP
|
// TOP
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(center[0], y - FIXED_BINDING_DISTANCE),
|
point(center[0], y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -984,7 +975,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// RIGHT
|
// RIGHT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
point(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -995,7 +986,7 @@ export const snapToMid = (
|
|||||||
) {
|
) {
|
||||||
// DOWN
|
// DOWN
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE),
|
point(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
@@ -1032,11 +1023,11 @@ const updateBoundPoint = (
|
|||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
const globalMidPoint = point<GlobalPoint>(
|
||||||
bindableElement.x + bindableElement.width / 2,
|
bindableElement.x + bindableElement.width / 2,
|
||||||
bindableElement.y + bindableElement.height / 2,
|
bindableElement.y + bindableElement.height / 2,
|
||||||
);
|
);
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = point<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,
|
||||||
);
|
);
|
||||||
@@ -1127,7 +1118,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
hoveredElement,
|
hoveredElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const globalMidPoint = pointFrom(
|
const globalMidPoint = point(
|
||||||
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,
|
||||||
);
|
);
|
||||||
@@ -1346,9 +1337,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(pointFrom(x, y), shape, threshold) ||
|
isPointOnShape(point(x, y), shape, threshold) ||
|
||||||
(fullShape === true &&
|
(fullShape === true &&
|
||||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
pointInsideBounds(point(x, y), aabbForElement(element)))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2206,11 +2197,11 @@ export const getGlobalFixedPointForBindableElement = (
|
|||||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||||
|
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
point(
|
||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
pointFrom<GlobalPoint>(
|
point<GlobalPoint>(
|
||||||
element.x + element.width / 2,
|
element.x + element.width / 2,
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
),
|
),
|
||||||
@@ -2238,7 +2229,7 @@ const getGlobalFixedPoints = (
|
|||||||
arrow.startBinding.fixedPoint,
|
arrow.startBinding.fixedPoint,
|
||||||
startElement as ExcalidrawBindableElement,
|
startElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: point<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],
|
||||||
);
|
);
|
||||||
@@ -2248,7 +2239,7 @@ const getGlobalFixedPoints = (
|
|||||||
arrow.endBinding.fixedPoint,
|
arrow.endBinding.fixedPoint,
|
||||||
endElement as ExcalidrawBindableElement,
|
endElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: point<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 { pointFrom } from "../../math";
|
import { point } 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: [
|
||||||
pointFrom<LocalPoint>(0, 0),
|
point<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(67.33984375, 92.48828125),
|
point<LocalPoint>(67.33984375, 92.48828125),
|
||||||
pointFrom<LocalPoint>(-102.7890625, 52.15625),
|
point<LocalPoint>(-102.7890625, 52.15625),
|
||||||
],
|
],
|
||||||
} as ExcalidrawLinearElement;
|
} as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
point,
|
||||||
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(
|
||||||
pointFrom(x, y),
|
point(x, y),
|
||||||
pointFrom(cx - element.x, cy - element.y),
|
point(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(
|
||||||
pointFrom(cx, y1),
|
point(cx, y1),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x12, y12] = pointRotateRads(
|
const [x12, y12] = pointRotateRads(
|
||||||
pointFrom(cx, y2),
|
point(cx, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x22, y22] = pointRotateRads(
|
const [x22, y22] = pointRotateRads(
|
||||||
pointFrom(x1, cy),
|
point(x1, cy),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x21, y21] = pointRotateRads(
|
const [x21, y21] = pointRotateRads(
|
||||||
pointFrom(x2, cy),
|
point(x2, cy),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x12, y12] = pointRotateRads(
|
const [x12, y12] = pointRotateRads(
|
||||||
pointFrom(x1, y2),
|
point(x1, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x22, y22] = pointRotateRads(
|
const [x22, y22] = pointRotateRads(
|
||||||
pointFrom(x2, y2),
|
point(x2, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [x21, y21] = pointRotateRads(
|
const [x21, y21] = pointRotateRads(
|
||||||
pointFrom(x2, y1),
|
point(x2, y1),
|
||||||
pointFrom(cx, cy),
|
point(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 = pointFrom(cx, cy);
|
const center: GlobalPoint = point(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(
|
||||||
pointFrom(
|
point(
|
||||||
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(
|
||||||
pointFrom(
|
point(
|
||||||
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 = pointFrom(0, 0);
|
let currentP: GlobalPoint = point(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 = pointFrom<GlobalPoint>(data[0], data[1]);
|
const _p1 = point<GlobalPoint>(data[0], data[1]);
|
||||||
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
|
const _p2 = point<GlobalPoint>(data[2], data[3]);
|
||||||
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
|
const _p3 = point<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 = pointFrom(data[4], data[5]);
|
const p3 = point(data[4], data[5]);
|
||||||
const p2 = pointFrom(data[2], data[3]);
|
const p2 = point(data[2], data[3]);
|
||||||
const p1 = pointFrom(data[0], data[1]);
|
const p1 = point(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 = pointFrom(0, 0);
|
let p0 = point(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 = pointFrom(prevOp.data[4], prevOp.data[5]);
|
p0 = point(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(
|
||||||
pointFrom(xs, ys),
|
point(xs, ys),
|
||||||
pointFrom(x2, y2),
|
point(x2, y2),
|
||||||
((-angle * Math.PI) / 180) as Radians,
|
((-angle * Math.PI) / 180) as Radians,
|
||||||
);
|
);
|
||||||
const [x4, y4] = pointRotateRads(
|
const [x4, y4] = pointRotateRads(
|
||||||
pointFrom(xs, ys),
|
point(xs, ys),
|
||||||
pointFrom(x2, y2),
|
point(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(
|
||||||
pointFrom(x2 + minSize * 2, y2),
|
point(x2 + minSize * 2, y2),
|
||||||
pointFrom(x2, y2),
|
point(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(
|
||||||
pointFrom(x2 - minSize * 2, y2),
|
point(x2 - minSize * 2, y2),
|
||||||
pointFrom(x2, y2),
|
point(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(
|
||||||
pointFrom(element.x + pointX, element.y + pointY),
|
point(element.x + pointX, element.y + pointY),
|
||||||
pointFrom(cx, cy),
|
point(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>(
|
||||||
pointFrom(element.x + x, element.y + y),
|
point(element.x + x, element.y + y),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
|
point((x1 + x2) / 2, (y1 + y2) / 2),
|
||||||
pointFrom(from.x, from.y),
|
point(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 =>
|
||||||
pointFrom(
|
point(
|
||||||
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, pointFrom } from "../../math";
|
import { isPointWithinBounds, point } 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(pointFrom(x, y), shape) ||
|
isPointInShape(point(x, y), shape) ||
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
isPointOnShape(point(x, y), shape, threshold)
|
||||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
: isPointOnShape(point(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(pointFrom(x, y), {
|
hit = isPointInShape(point(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,11 +89,7 @@ export const hitElementBoundingBox = (
|
|||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
||||||
pointFrom(x1, y1),
|
|
||||||
pointFrom(x, y),
|
|
||||||
pointFrom(x2, y2),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = <
|
export const hitElementBoundingBoxOnly = <
|
||||||
@@ -119,5 +115,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(pointFrom(x, y), textShape);
|
return !!textShape && isPointInShape(point(x, y), textShape);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,12 +45,6 @@ const RE_GENERIC_EMBED =
|
|||||||
const RE_GIPHY =
|
const RE_GIPHY =
|
||||||
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||||
|
|
||||||
const RE_REDDIT =
|
|
||||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
|
|
||||||
|
|
||||||
const RE_REDDIT_EMBED =
|
|
||||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -65,7 +59,6 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
"giphy.com",
|
"giphy.com",
|
||||||
"reddit.com",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@@ -78,7 +71,6 @@ const ALLOW_SAME_ORIGIN = new Set([
|
|||||||
"x.com",
|
"x.com",
|
||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"reddit.com",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const createSrcDoc = (body: string) => {
|
export const createSrcDoc = (body: string) => {
|
||||||
@@ -226,24 +218,6 @@ export const getEmbedLink = (
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RE_REDDIT.test(link)) {
|
|
||||||
const [, page, postId, title] = link.match(RE_REDDIT)!;
|
|
||||||
const safeURL = sanitizeHTMLAttribute(
|
|
||||||
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
|
|
||||||
);
|
|
||||||
const ret: IframeDataWithSandbox = {
|
|
||||||
type: "document",
|
|
||||||
srcdoc: (theme: string) =>
|
|
||||||
createSrcDoc(
|
|
||||||
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
|
|
||||||
),
|
|
||||||
intrinsicSize: { w: 480, h: 480 },
|
|
||||||
sandbox: { allowSameOrigin },
|
|
||||||
};
|
|
||||||
embeddedLinkCache.set(originalLink, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RE_GH_GIST.test(link)) {
|
if (RE_GH_GIST.test(link)) {
|
||||||
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
||||||
const safeURL = sanitizeHTMLAttribute(
|
const safeURL = sanitizeHTMLAttribute(
|
||||||
@@ -387,11 +361,6 @@ export const maybeParseEmbedSrc = (str: string): string => {
|
|||||||
return twitterMatch[1];
|
return twitterMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const redditMatch = str.match(RE_REDDIT_EMBED);
|
|
||||||
if (redditMatch && redditMatch.length === 2) {
|
|
||||||
return redditMatch[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
||||||
if (gistMatch && gistMatch.length === 2) {
|
if (gistMatch && gistMatch.length === 2) {
|
||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { invariant } from "../utils";
|
import { invariant } from "../utils";
|
||||||
import { pointFrom, type LocalPoint } from "../../math";
|
import { point, 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: [pointFrom(0, 0), pointFrom(endX, endY)],
|
points: [point(0, 0), point(endX, endY)],
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
Radians,
|
Radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import {
|
import {
|
||||||
pointFrom,
|
point,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointScaleFromOrigin,
|
pointScaleFromOrigin,
|
||||||
radiansToDegrees,
|
radiansToDegrees,
|
||||||
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
|
|||||||
|
|
||||||
const top = pointRotateRads(
|
const top = pointRotateRads(
|
||||||
pointScaleFromOrigin(
|
pointScaleFromOrigin(
|
||||||
pointFrom(element.x + element.width / 2, element.y),
|
point(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(
|
||||||
pointFrom(element.x + element.width, element.y + element.height / 2),
|
point(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(
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height),
|
point(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(
|
||||||
pointFrom(element.x, element.y + element.height / 2),
|
point(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(
|
||||||
pointFrom(aabb[0], aabb[1]),
|
point(aabb[0], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const topRight = pointScaleFromOrigin(
|
const topRight = pointScaleFromOrigin(
|
||||||
pointFrom(aabb[2], aabb[1]),
|
point(aabb[2], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const bottomLeft = pointScaleFromOrigin(
|
const bottomLeft = pointScaleFromOrigin(
|
||||||
pointFrom(aabb[0], aabb[3]),
|
point(aabb[0], aabb[3]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
const bottomRight = pointScaleFromOrigin(
|
const bottomRight = pointScaleFromOrigin(
|
||||||
pointFrom(aabb[2], aabb[3]),
|
point(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,
|
||||||
pointFrom,
|
point,
|
||||||
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], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], point(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,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
point(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: pointFrom(
|
point: point(
|
||||||
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(),
|
||||||
)
|
)
|
||||||
: pointFrom(
|
: point(
|
||||||
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(
|
||||||
pointFrom(
|
point(
|
||||||
existingSegmentMidpointHitCoords[0],
|
existingSegmentMidpointHitCoords[0],
|
||||||
existingSegmentMidpointHitCoords[1],
|
existingSegmentMidpointHitCoords[1],
|
||||||
),
|
),
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
point(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(
|
||||||
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
point(midPoints[index]![0], midPoints[index]![1]),
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
point(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(
|
||||||
pointFrom(startPoint[0], startPoint[1]),
|
point(startPoint[0], startPoint[1]),
|
||||||
pointFrom(endPoint[0], endPoint[1]),
|
point(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(
|
||||||
pointFrom(
|
point(
|
||||||
element.x + element.points[clickedPointIndex][0],
|
element.x + element.points[clickedPointIndex][0],
|
||||||
element.y + element.points[clickedPointIndex][1],
|
element.y + element.points[clickedPointIndex][1],
|
||||||
),
|
),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -928,11 +928,11 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
point(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
newPoint = pointFrom(
|
newPoint = point(
|
||||||
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(
|
||||||
pointFrom(x + p[0], y + p[1]),
|
point(x + p[0], y + p[1]),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
||||||
pointFrom(x + p[0], y + p[1]),
|
point(x + p[0], y + p[1]),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1025,12 +1025,8 @@ export class LinearElementEditor {
|
|||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
|
|
||||||
return p
|
return p
|
||||||
? pointRotateRads(
|
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
|
||||||
pointFrom(x + p[0], y + p[1]),
|
: pointRotateRads(point(x, y), point(cx, cy), element.angle);
|
||||||
pointFrom(cx, cy),
|
|
||||||
element.angle,
|
|
||||||
)
|
|
||||||
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static pointFromAbsoluteCoords(
|
static pointFromAbsoluteCoords(
|
||||||
@@ -1040,7 +1036,7 @@ export class LinearElementEditor {
|
|||||||
): LocalPoint {
|
): LocalPoint {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
// No rotation for elbow arrows
|
// No rotation for elbow arrows
|
||||||
return pointFrom(
|
return point(
|
||||||
absoluteCoords[0] - element.x,
|
absoluteCoords[0] - element.x,
|
||||||
absoluteCoords[1] - element.y,
|
absoluteCoords[1] - element.y,
|
||||||
);
|
);
|
||||||
@@ -1050,11 +1046,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(
|
||||||
pointFrom(absoluteCoords[0], absoluteCoords[1]),
|
point(absoluteCoords[0], absoluteCoords[1]),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
return pointFrom(x - element.x, y - element.y);
|
return point(x - element.x, y - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPointIndexUnderCursor(
|
static getPointIndexUnderCursor(
|
||||||
@@ -1075,7 +1071,7 @@ export class LinearElementEditor {
|
|||||||
while (--idx > -1) {
|
while (--idx > -1) {
|
||||||
const p = pointHandles[idx];
|
const p = pointHandles[idx];
|
||||||
if (
|
if (
|
||||||
pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value <
|
pointDistance(point(x, y), point(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
|
||||||
) {
|
) {
|
||||||
@@ -1097,12 +1093,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(
|
||||||
pointFrom(pointerOnGrid[0], pointerOnGrid[1]),
|
point(pointerOnGrid[0], pointerOnGrid[1]),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
return pointFrom(rotatedX - element.x, rotatedY - element.y);
|
return point(rotatedX - element.x, rotatedY - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1122,7 +1118,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
points: points.map((p) => {
|
points: points.map((p) => {
|
||||||
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
return point(p[0] - offsetX, p[1] - offsetY);
|
||||||
}),
|
}),
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
@@ -1176,8 +1172,8 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
acc.push(
|
acc.push(
|
||||||
nextPoint
|
nextPoint
|
||||||
? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||||
: pointFrom(p[0], p[1]),
|
: point(p[0], p[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
nextSelectedIndices.push(indexCursor + 1);
|
nextSelectedIndices.push(indexCursor + 1);
|
||||||
@@ -1198,7 +1194,7 @@ export class LinearElementEditor {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
point: point(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -1239,9 +1235,7 @@ 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
|
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
|
||||||
? pointFrom(0, 0)
|
|
||||||
: pointFrom(p[0] - offsetX, p[1] - offsetY),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1318,9 +1312,9 @@ export class LinearElementEditor {
|
|||||||
const deltaY =
|
const deltaY =
|
||||||
selectedPointData.point[1] - points[selectedPointData.index][1];
|
selectedPointData.point[1] - points[selectedPointData.index][1];
|
||||||
|
|
||||||
return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
||||||
}
|
}
|
||||||
return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p;
|
return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p;
|
||||||
});
|
});
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
@@ -1374,8 +1368,8 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const origin = linearElementEditor.pointerDownState.origin!;
|
const origin = linearElementEditor.pointerDownState.origin!;
|
||||||
const dist = pointDistance(
|
const dist = pointDistance(
|
||||||
pointFrom(origin.x, origin.y),
|
point(origin.x, origin.y),
|
||||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
point(pointerCoords.x, pointerCoords.y),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
@@ -1499,8 +1493,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(
|
||||||
pointFrom(offsetX, offsetY),
|
point(offsetX, offsetY),
|
||||||
pointFrom(dX, dY),
|
point(dX, dY),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -1546,8 +1540,8 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(width, height),
|
point(width, height),
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1617,36 +1611,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 = pointFrom(cx, cy);
|
const centerPoint = point(cx, cy);
|
||||||
|
|
||||||
const topLeftRotatedPoint = pointRotateRads(
|
const topLeftRotatedPoint = pointRotateRads(
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topRightRotatedPoint = pointRotateRads(
|
const topRightRotatedPoint = pointRotateRads(
|
||||||
pointFrom(x2, y1),
|
point(x2, y1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
const counterRotateBoundTextTopLeft = pointRotateRads(
|
const counterRotateBoundTextTopLeft = pointRotateRads(
|
||||||
pointFrom(boundTextX1, boundTextY1),
|
point(boundTextX1, boundTextY1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextTopRight = pointRotateRads(
|
const counterRotateBoundTextTopRight = pointRotateRads(
|
||||||
pointFrom(boundTextX2, boundTextY1),
|
point(boundTextX2, boundTextY1),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
||||||
pointFrom(boundTextX1, boundTextY2),
|
point(boundTextX1, boundTextY2),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomRight = pointRotateRads(
|
const counterRotateBoundTextBottomRight = pointRotateRads(
|
||||||
pointFrom(boundTextX2, boundTextY2),
|
point(boundTextX2, boundTextY2),
|
||||||
centerPoint,
|
centerPoint,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,23 +5,12 @@ 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
|
||||||
@@ -32,8 +21,6 @@ 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)
|
||||||
@@ -82,7 +69,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!didChangePoints) {
|
if (!didChangePoints) {
|
||||||
key in oldUpdates && (increment = true);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +76,6 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +92,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (increment) {
|
element.version++;
|
||||||
element.version++;
|
element.versionNonce = randomInteger();
|
||||||
element.versionNonce = randomInteger();
|
element.updated = getUpdatedTimestamp();
|
||||||
element.updated = getUpdatedTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (informMutation) {
|
if (informMutation) {
|
||||||
Scene.getScene(element)?.triggerUpdate();
|
Scene.getScene(element)?.triggerUpdate();
|
||||||
@@ -127,8 +110,6 @@ 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") {
|
||||||
@@ -140,7 +121,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
didChange = true;
|
didChange = true;
|
||||||
key in oldUpdates && (increment = true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,9 +128,6 @@ 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 { pointFrom } from "../../math";
|
import { point } 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: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element);
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
import {
|
||||||
|
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";
|
||||||
@@ -27,9 +32,9 @@ import type { AppState } from "../types";
|
|||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
measureTextElement,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapTextElement,
|
wrapText,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
@@ -43,30 +48,6 @@ 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">,
|
||||||
@@ -81,8 +62,6 @@ export type ElementConstructorOpts = MarkOptional<
|
|||||||
| "version"
|
| "version"
|
||||||
| "versionNonce"
|
| "versionNonce"
|
||||||
| "link"
|
| "link"
|
||||||
| "subtype"
|
|
||||||
| "customData"
|
|
||||||
| "strokeStyle"
|
| "strokeStyle"
|
||||||
| "fillStyle"
|
| "fillStyle"
|
||||||
| "strokeColor"
|
| "strokeColor"
|
||||||
@@ -120,10 +99,8 @@ 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,
|
||||||
@@ -159,11 +136,8 @@ export const newElement = (
|
|||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawGenericElement["type"];
|
type: ExcalidrawGenericElement["type"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawGenericElement> => {
|
): NonDeleted<ExcalidrawGenericElement> =>
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
map?.clean && map.clean(opts);
|
|
||||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
@@ -256,12 +230,10 @@ 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 = measureTextElement(
|
const metrics = measureText(
|
||||||
{ ...opts, fontSize, fontFamily, lineHeight },
|
text,
|
||||||
{
|
getFontString({ fontFamily, fontSize }),
|
||||||
text,
|
lineHeight,
|
||||||
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;
|
||||||
@@ -305,9 +277,11 @@ const getAdjustedDimensions = (
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
} => {
|
} => {
|
||||||
let { width: nextWidth, height: nextHeight } = measureTextElement(element, {
|
let { width: nextWidth, height: nextHeight } = measureText(
|
||||||
text: nextText,
|
nextText,
|
||||||
});
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
// wrapped text
|
// wrapped text
|
||||||
if (!element.autoResize) {
|
if (!element.autoResize) {
|
||||||
@@ -323,7 +297,11 @@ const getAdjustedDimensions = (
|
|||||||
!element.containerId &&
|
!element.containerId &&
|
||||||
element.autoResize
|
element.autoResize
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureTextElement(element);
|
const prevMetrics = measureText(
|
||||||
|
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,
|
||||||
@@ -426,14 +404,12 @@ export const refreshTextDimensions = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (container || !textElement.autoResize) {
|
if (container || !textElement.autoResize) {
|
||||||
text = wrapTextElement(
|
text = wrapText(
|
||||||
textElement,
|
text,
|
||||||
|
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);
|
||||||
@@ -448,8 +424,6 @@ 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 || [],
|
||||||
@@ -465,8 +439,6 @@ 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 || [],
|
||||||
@@ -487,8 +459,6 @@ 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 || [],
|
||||||
@@ -509,8 +479,6 @@ 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,
|
||||||
pointFrom,
|
point,
|
||||||
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(
|
||||||
pointFrom(pointerX, pointerY),
|
point(pointerX, pointerY),
|
||||||
pointFrom(cx, cy),
|
point(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 = pointFrom<GlobalPoint>(x1, y1);
|
let newTopLeft = point<GlobalPoint>(x1, y1);
|
||||||
if (["n", "w", "nw"].includes(transformHandleType)) {
|
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||||
newTopLeft = pointFrom<GlobalPoint>(
|
newTopLeft = point<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 = pointFrom<GlobalPoint>(
|
newTopLeft = point<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 = pointFrom<GlobalPoint>(
|
newTopLeft = point<GlobalPoint>(
|
||||||
topRight[0] - Math.abs(nextWidth),
|
topRight[0] - Math.abs(nextWidth),
|
||||||
topRight[1],
|
topRight[1],
|
||||||
);
|
);
|
||||||
@@ -311,20 +311,12 @@ const resizeSingleTextElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const angle = element.angle;
|
const angle = element.angle;
|
||||||
const rotatedTopLeft = pointRotateRads(
|
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
|
||||||
newTopLeft,
|
const newCenter = point<GlobalPoint>(
|
||||||
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(
|
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
|
||||||
newCenter,
|
|
||||||
pointFrom(cx, cy),
|
|
||||||
angle,
|
|
||||||
);
|
|
||||||
newTopLeft = pointRotateRads(
|
newTopLeft = pointRotateRads(
|
||||||
rotatedTopLeft,
|
rotatedTopLeft,
|
||||||
rotatedNewCenter,
|
rotatedNewCenter,
|
||||||
@@ -349,12 +341,12 @@ const resizeSingleTextElement = (
|
|||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
const startTopLeft = point<GlobalPoint>(x1, y1);
|
||||||
const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
|
const startBottomRight = point<GlobalPoint>(x2, y2);
|
||||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
point(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle as Radians,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
@@ -427,7 +419,7 @@ const resizeSingleTextElement = (
|
|||||||
startCenter,
|
startCenter,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
const newCenter = pointFrom(
|
const newCenter = point(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
);
|
);
|
||||||
@@ -469,13 +461,13 @@ export const resizeSingleElement = (
|
|||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft = pointFrom(x1, y1);
|
const startTopLeft = point(x1, y1);
|
||||||
const startBottomRight = pointFrom(x2, y2);
|
const startBottomRight = point(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(
|
||||||
pointFrom(pointerX, pointerY),
|
point(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle as Radians,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
@@ -656,7 +648,7 @@ export const resizeSingleElement = (
|
|||||||
startCenter,
|
startCenter,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
const newCenter = pointFrom(
|
const newCenter = point(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
);
|
);
|
||||||
@@ -825,20 +817,20 @@ export const resizeMultipleElements = (
|
|||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
||||||
ne: pointFrom(minX, maxY),
|
ne: point(minX, maxY),
|
||||||
se: pointFrom(minX, minY),
|
se: point(minX, minY),
|
||||||
sw: pointFrom(maxX, minY),
|
sw: point(maxX, minY),
|
||||||
nw: pointFrom(maxX, maxY),
|
nw: point(maxX, maxY),
|
||||||
e: pointFrom(minX, minY + height / 2),
|
e: point(minX, minY + height / 2),
|
||||||
w: pointFrom(maxX, minY + height / 2),
|
w: point(maxX, minY + height / 2),
|
||||||
n: pointFrom(minX + width / 2, maxY),
|
n: point(minX + width / 2, maxY),
|
||||||
s: pointFrom(minX + width / 2, minY),
|
s: point(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
|
||||||
? pointFrom(midX, midY)
|
? point(midX, midY)
|
||||||
: anchorsMap[direction];
|
: anchorsMap[direction];
|
||||||
|
|
||||||
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||||
@@ -1052,8 +1044,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(
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
pointFrom(centerX, centerY),
|
point(centerX, centerY),
|
||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1109,44 +1101,40 @@ 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(
|
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
|
||||||
pointFrom(x, y),
|
|
||||||
pointFrom(cx, cy),
|
|
||||||
-angle as Radians,
|
|
||||||
);
|
|
||||||
switch (transformHandleType) {
|
switch (transformHandleType) {
|
||||||
case "n":
|
case "n":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x - (x1 + x2) / 2, y - y1),
|
point(x - (x1 + x2) / 2, y - y1),
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "s":
|
case "s":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x - (x1 + x2) / 2, y - y2),
|
point(x - (x1 + x2) / 2, y - y2),
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "w":
|
case "w":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x - x1, y - (y1 + y2) / 2),
|
point(x - x1, y - (y1 + y2) / 2),
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "e":
|
case "e":
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(x - x2, y - (y1 + y2) / 2),
|
point(x - x2, y - (y1 + y2) / 2),
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
case "nw":
|
case "nw":
|
||||||
return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
|
return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle);
|
||||||
case "ne":
|
case "ne":
|
||||||
return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
|
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
|
||||||
case "sw":
|
case "sw":
|
||||||
return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
|
return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle);
|
||||||
case "se":
|
case "se":
|
||||||
return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
|
return pointRotateRads(point(x - x2, y - y2), point(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 {
|
||||||
pointFrom,
|
point,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type Radians,
|
type Radians,
|
||||||
@@ -92,20 +92,16 @@ 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(
|
||||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
point(x1 - SPACING, y1 - SPACING),
|
||||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
point(x2 + SPACING, y2 + SPACING),
|
||||||
pointFrom(cx, cy),
|
point(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(
|
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
||||||
pointFrom(x, y),
|
|
||||||
side as LineSegment<Point>,
|
|
||||||
SPACING,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return dir as TransformHandleType;
|
return dir as TransformHandleType;
|
||||||
}
|
}
|
||||||
@@ -182,9 +178,9 @@ export const getTransformHandleTypeFromCoords = <
|
|||||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
|
|
||||||
const sides = getSelectionBorders(
|
const sides = getSelectionBorders(
|
||||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
point(x1 - SPACING, y1 - SPACING),
|
||||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
point(x2 + SPACING, y2 + SPACING),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
0 as Radians,
|
0 as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -192,7 +188,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(
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
point(scenePointerX, scenePointerY),
|
||||||
side as LineSegment<Point>,
|
side as LineSegment<Point>,
|
||||||
SPACING,
|
SPACING,
|
||||||
)
|
)
|
||||||
@@ -269,10 +265,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
center: Point,
|
center: Point,
|
||||||
angle: Radians,
|
angle: Radians,
|
||||||
) => {
|
) => {
|
||||||
const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
|
const topLeft = pointRotateRads(point(x1, y1), center, angle);
|
||||||
const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
|
const topRight = pointRotateRads(point(x2, y1), center, angle);
|
||||||
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
|
const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
|
||||||
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
|
const bottomRight = pointRotateRads(point(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 { pointFrom } from "../../math";
|
import { point } 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(), [
|
||||||
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
point(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
point(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: [pointFrom(0, 0), pointFrom(90, 200)],
|
points: [point(0, 0), point(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, [pointFrom(0, 0), pointFrom(90, 200)]);
|
mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(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 {
|
||||||
pointFrom,
|
point,
|
||||||
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 pointFrom(p[0], bounds[1]);
|
return point(p[0], bounds[1]);
|
||||||
case HEADING_RIGHT:
|
case HEADING_RIGHT:
|
||||||
return pointFrom(bounds[2], p[1]);
|
return point(bounds[2], p[1]);
|
||||||
case HEADING_DOWN:
|
case HEADING_DOWN:
|
||||||
return pointFrom(p[0], bounds[3]);
|
return point(p[0], bounds[3]);
|
||||||
}
|
}
|
||||||
return pointFrom(bounds[0], p[1]);
|
return point(bounds[0], p[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const estimateSegmentCount = (
|
const estimateSegmentCount = (
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
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]);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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,5 +1,3 @@
|
|||||||
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,
|
||||||
@@ -32,30 +30,6 @@ 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)
|
||||||
@@ -90,12 +64,18 @@ export const redrawTextBoundingBox = (
|
|||||||
maxWidth = container
|
maxWidth = container
|
||||||
? getBoundTextMaxWidth(container, textElement)
|
? getBoundTextMaxWidth(container, textElement)
|
||||||
: textElement.width;
|
: textElement.width;
|
||||||
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
boundTextUpdates.text = wrapText(
|
||||||
|
textElement.originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
maxWidth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = measureTextElement(textElement, {
|
const metrics = measureText(
|
||||||
text: boundTextUpdates.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) {
|
||||||
@@ -103,14 +83,6 @@ 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,
|
||||||
@@ -219,9 +191,17 @@ export const handleBindTextResize = (
|
|||||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
(transformHandleType !== "n" && transformHandleType !== "s")
|
||||||
) {
|
) {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapTextElement(textElement, maxWidth);
|
text = wrapText(
|
||||||
|
textElement.originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
maxWidth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureTextElement(textElement, { text });
|
const metrics = measureText(
|
||||||
|
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 { pointFrom } from "../../math";
|
import { point } 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: [pointFrom(0, 0), pointFrom(100, 0)],
|
points: [point(0, 0), point(100, 0)],
|
||||||
});
|
});
|
||||||
const textSize = 20;
|
const textSize = 20;
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
@@ -47,15 +46,12 @@ 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,
|
||||||
@@ -73,18 +69,9 @@ 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;
|
||||||
}
|
}
|
||||||
const offset = offsetX !== 0 ? ` translate(${offsetX}px, 0px)` : "";
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
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,
|
||||||
@@ -150,27 +137,14 @@ export const textWysiwyg = ({
|
|||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Editing metrics
|
let width = updatedTextElement.width;
|
||||||
const eMetrics = measureText(
|
|
||||||
container && updatedTextElement.containerId
|
|
||||||
? wrapText(
|
|
||||||
updatedTextElement.originalText,
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
getBoundTextMaxWidth(container, updatedTextElement),
|
|
||||||
)
|
|
||||||
: updatedTextElement.originalText,
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
updatedTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
let width = Math.max(updatedTextElement.width, eMetrics.width);
|
// set to element height by default since that's
|
||||||
|
|
||||||
// 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 = Math.max(updatedTextElement.height, eMetrics.height);
|
let height = updatedTextElement.height;
|
||||||
|
|
||||||
let maxWidth = width;
|
let maxWidth = updatedTextElement.width;
|
||||||
let maxHeight = height;
|
let maxHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
@@ -266,31 +240,8 @@ 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);
|
||||||
|
|
||||||
@@ -310,9 +261,7 @@ 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),
|
||||||
@@ -373,7 +322,6 @@ 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 { pointFrom, pointRotateRads } from "../../math";
|
import { point, 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(
|
||||||
pointFrom(x + width / 2, y + height / 2),
|
point(x + width / 2, y + height / 2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
return [xx - width / 2, yy - height / 2, width, height];
|
return [xx - width / 2, yy - height / 2, width, height];
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ 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>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
|
|||||||
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
||||||
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
||||||
|
|
||||||
import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
||||||
import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||||
|
|
||||||
import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
||||||
import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
||||||
import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
||||||
import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
||||||
import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
|
|||||||
@@ -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, pointFrom } from "../math";
|
import { isPointWithinBounds, point } 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(
|
||||||
pointFrom(fx1, fy1),
|
point(fx1, fy1),
|
||||||
pointFrom(cursorCoords.x, cursorCoords.y),
|
point(cursorCoords.x, cursorCoords.y),
|
||||||
pointFrom(fx2, fy2),
|
point(fx2, fy2),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
-2
@@ -104,5 +104,3 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax";
|
|
||||||
|
|||||||
@@ -87,17 +87,6 @@ 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";
|
||||||
@@ -112,14 +101,6 @@ 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);
|
||||||
@@ -142,9 +123,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path:
|
path: NestedKeyOf<typeof fallbackLangData>,
|
||||||
| NestedKeyOf<typeof fallbackLangData>
|
|
||||||
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
|
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -159,7 +138,6 @@ 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}`;
|
||||||
|
|||||||
@@ -230,7 +230,8 @@
|
|||||||
"resetLibrary": "This will clear your library. Are you sure?",
|
"resetLibrary": "This will clear your library. Are you sure?",
|
||||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
||||||
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
|
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!",
|
||||||
|
"saveYourContent": "Don't forget to save your content ({{shortcut}})!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Unsupported file type.",
|
"unsupportedFileType": "Unsupported file type.",
|
||||||
|
|||||||
@@ -72,7 +72,6 @@
|
|||||||
"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,6 +23,7 @@ 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";
|
||||||
@@ -36,7 +37,6 @@ 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,14 +251,7 @@ const generateElementCanvas = (
|
|||||||
context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
rc,
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
@@ -381,22 +374,11 @@ 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":
|
||||||
@@ -691,7 +673,7 @@ export const renderSelectionElement = (
|
|||||||
|
|
||||||
export const renderElement = (
|
export const renderElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: RenderableElementsMap,
|
||||||
allElementsMap: NonDeletedSceneElementsMap,
|
allElementsMap: NonDeletedSceneElementsMap,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -760,14 +742,7 @@ 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(
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
rc,
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
@@ -862,7 +837,6 @@ export const renderElement = (
|
|||||||
|
|
||||||
drawElementOnCanvas(
|
drawElementOnCanvas(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
tempRc,
|
tempRc,
|
||||||
tempCanvasContext,
|
tempCanvasContext,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
@@ -906,14 +880,7 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
rc,
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math";
|
import { point, 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,31 +140,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(from[0], from[1] - FULL),
|
point(from[0], from[1] - FULL),
|
||||||
pointFrom(from[0], from[1] + FULL),
|
point(from[0], from[1] + FULL),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
point(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
||||||
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
point(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
point(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
||||||
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
point(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine(
|
drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context);
|
||||||
pointFrom(to[0], to[1] - FULL),
|
|
||||||
pointFrom(to[0], to[1] + FULL),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
@@ -174,31 +170,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(from[0] - FULL, from[1]),
|
point(from[0] - FULL, from[1]),
|
||||||
pointFrom(from[0] + FULL, from[1]),
|
point(from[0] + FULL, from[1]),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
point(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
||||||
pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
point(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
point(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
||||||
pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
point(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine(
|
drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context);
|
||||||
pointFrom(to[0] - FULL, to[1]),
|
|
||||||
pointFrom(to[0] + FULL, to[1]),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ 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";
|
||||||
|
|
||||||
@@ -126,15 +125,6 @@ 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 {
|
||||||
pointFrom,
|
point,
|
||||||
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
|
||||||
: [pointFrom<LocalPoint>(0, 0)];
|
: [point<LocalPoint>(0, 0)];
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
shape = [
|
shape = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointFrom,
|
point,
|
||||||
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,
|
||||||
pointFrom<Point>(element.x, element.y),
|
point<Point>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
)
|
)
|
||||||
: getCurveShape<Point>(
|
: getCurveShape<Point>(
|
||||||
roughShape,
|
roughShape,
|
||||||
pointFrom<Point>(element.x, element.y),
|
point<Point>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
pointFrom(cx, cy),
|
point(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,
|
||||||
pointFrom(cx, cy),
|
point(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 = pointFrom<P>(0, 0);
|
let currentP = point<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 = pointFrom<P>(data[0], data[1]);
|
const p1 = point<P>(data[0], data[1]);
|
||||||
const p2 = pointFrom<P>(data[2], data[3]);
|
const p2 = point<P>(data[2], data[3]);
|
||||||
const p3 = pointFrom<P>(data[4], data[5]);
|
const p3 = point<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 pointFrom(tx, ty);
|
return point(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(pointFrom(p[0], p[1]));
|
pointsOnCurve.push(point(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(pointFrom(endPoint[0], endPoint[1]));
|
pointsOnCurve.push(point(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 = pointFrom(bbox.midX, bbox.midY);
|
const center = point(bbox.midX, bbox.midY);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
point(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [topRightX, topRightY] = pointRotateRads(
|
const [topRightX, topRightY] = pointRotateRads(
|
||||||
pointFrom(bbox.maxX, bbox.minY),
|
point(bbox.maxX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [bottomRightX, bottomRightY] = pointRotateRads(
|
const [bottomRightX, bottomRightY] = pointRotateRads(
|
||||||
pointFrom(bbox.maxX, bbox.maxY),
|
point(bbox.maxX, bbox.maxY),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.maxY),
|
point(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(pointFrom(a[0], a[1]), b) ||
|
pointInsideBounds(point(a[0], a[1]), b) ||
|
||||||
pointInsideBounds(pointFrom(a[2], a[1]), b) ||
|
pointInsideBounds(point(a[2], a[1]), b) ||
|
||||||
pointInsideBounds(pointFrom(a[2], a[3]), b) ||
|
pointInsideBounds(point(a[2], a[3]), b) ||
|
||||||
pointInsideBounds(pointFrom(a[0], a[3]), b) ||
|
pointInsideBounds(point(a[0], a[3]), b) ||
|
||||||
pointInsideBounds(pointFrom(b[0], b[1]), a) ||
|
pointInsideBounds(point(b[0], b[1]), a) ||
|
||||||
pointInsideBounds(pointFrom(b[2], b[1]), a) ||
|
pointInsideBounds(point(b[2], b[1]), a) ||
|
||||||
pointInsideBounds(pointFrom(b[2], b[3]), a) ||
|
pointInsideBounds(point(b[2], b[3]), a) ||
|
||||||
pointInsideBounds(pointFrom(b[0], b[3]), a);
|
pointInsideBounds(point(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 {
|
||||||
pointFrom,
|
point,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
rangeIntersection,
|
rangeIntersection,
|
||||||
@@ -228,52 +228,52 @@ export const getElementsCorners = (
|
|||||||
!boundingBoxCorners
|
!boundingBoxCorners
|
||||||
) {
|
) {
|
||||||
const leftMid = pointRotateRads<GlobalPoint>(
|
const leftMid = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x1, y1 + halfHeight),
|
point(x1, y1 + halfHeight),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topMid = pointRotateRads<GlobalPoint>(
|
const topMid = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x1 + halfWidth, y1),
|
point(x1 + halfWidth, y1),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const rightMid = pointRotateRads<GlobalPoint>(
|
const rightMid = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x2, y1 + halfHeight),
|
point(x2, y1 + halfHeight),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomMid = pointRotateRads<GlobalPoint>(
|
const bottomMid = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x1 + halfWidth, y2),
|
point(x1 + halfWidth, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
const center = point<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>(
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topRight = pointRotateRads<GlobalPoint>(
|
const topRight = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x2, y1),
|
point(x2, y1),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomLeft = pointRotateRads<GlobalPoint>(
|
const bottomLeft = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x1, y2),
|
point(x1, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomRight = pointRotateRads<GlobalPoint>(
|
const bottomRight = pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x2, y2),
|
point(x2, y2),
|
||||||
pointFrom(cx, cy),
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
const center = point<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 = pointFrom<GlobalPoint>(minX, minY);
|
const topLeft = point<GlobalPoint>(minX, minY);
|
||||||
const topRight = pointFrom<GlobalPoint>(maxX, minY);
|
const topRight = point<GlobalPoint>(maxX, minY);
|
||||||
const bottomLeft = pointFrom<GlobalPoint>(minX, maxY);
|
const bottomLeft = point<GlobalPoint>(minX, maxY);
|
||||||
const bottomRight = pointFrom<GlobalPoint>(maxX, maxY);
|
const bottomRight = point<GlobalPoint>(maxX, maxY);
|
||||||
const center = pointFrom<GlobalPoint>(minX + width / 2, minY + height / 2);
|
const center = point<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) => pointFrom(round(p[0]), round(p[1])));
|
return result.map((p) => point(round(p[0]), round(p[1])));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReferenceElements = (
|
const getReferenceElements = (
|
||||||
@@ -375,11 +375,8 @@ export const getVisibleGaps = (
|
|||||||
horizontalGaps.push({
|
horizontalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [
|
startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
|
||||||
pointFrom(startMaxX, startMinY),
|
endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
|
||||||
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),
|
||||||
@@ -418,11 +415,8 @@ export const getVisibleGaps = (
|
|||||||
verticalGaps.push({
|
verticalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [
|
startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
|
||||||
pointFrom(startMinX, startMaxY),
|
endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
|
||||||
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),
|
||||||
@@ -838,7 +832,7 @@ const createPointSnapLines = (
|
|||||||
}
|
}
|
||||||
snapsX[key].push(
|
snapsX[key].push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
point<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -855,7 +849,7 @@ const createPointSnapLines = (
|
|||||||
}
|
}
|
||||||
snapsY[key].push(
|
snapsY[key].push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
point<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -869,7 +863,7 @@ const createPointSnapLines = (
|
|||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
return pointFrom<GlobalPoint>(Number(key), p[1]);
|
return point<GlobalPoint>(Number(key), p[1]);
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[1] - b[1]),
|
.sort((a, b) => a[1] - b[1]),
|
||||||
),
|
),
|
||||||
@@ -882,7 +876,7 @@ const createPointSnapLines = (
|
|||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
return pointFrom<GlobalPoint>(p[0], Number(key));
|
return point<GlobalPoint>(p[0], Number(key));
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[0] - b[0]),
|
.sort((a, b) => a[0] - b[0]),
|
||||||
),
|
),
|
||||||
@@ -946,16 +940,16 @@ const createGapSnapLines = (
|
|||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
pointFrom(gapSnap.gap.startSide[0][0], gapLineY),
|
point(gapSnap.gap.startSide[0][0], gapLineY),
|
||||||
pointFrom(minX, gapLineY),
|
point(minX, gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
pointFrom(maxX, gapLineY),
|
point(maxX, gapLineY),
|
||||||
pointFrom(gapSnap.gap.endSide[0][0], gapLineY),
|
point(gapSnap.gap.endSide[0][0], gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -972,16 +966,16 @@ const createGapSnapLines = (
|
|||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
pointFrom(gapLineX, gapSnap.gap.startSide[0][1]),
|
point(gapLineX, gapSnap.gap.startSide[0][1]),
|
||||||
pointFrom(gapLineX, minY),
|
point(gapLineX, minY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
pointFrom(gapLineX, maxY),
|
point(gapLineX, maxY),
|
||||||
pointFrom(gapLineX, gapSnap.gap.endSide[0][1]),
|
point(gapLineX, gapSnap.gap.endSide[0][1]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -997,15 +991,12 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||||
pointFrom(startMaxX, gapLineY),
|
|
||||||
pointFrom(endMinX, gapLineY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)],
|
points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1020,18 +1011,12 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
|
||||||
pointFrom(maxX, gapLineY),
|
|
||||||
pointFrom(startMinX, gapLineY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||||
pointFrom(startMaxX, gapLineY),
|
|
||||||
pointFrom(endMinX, gapLineY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1046,18 +1031,12 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
|
||||||
pointFrom(gapLineX, maxY),
|
|
||||||
pointFrom(gapLineX, startMinY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||||
pointFrom(gapLineX, startMaxY),
|
|
||||||
pointFrom(gapLineX, endMinY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1072,15 +1051,12 @@ const createGapSnapLines = (
|
|||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||||
pointFrom(gapLineX, startMaxY),
|
|
||||||
pointFrom(gapLineX, endMinY),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)],
|
points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1094,7 +1070,7 @@ const createGapSnapLines = (
|
|||||||
return {
|
return {
|
||||||
...gapSnapLine,
|
...gapSnapLine,
|
||||||
points: gapSnapLine.points.map((p) =>
|
points: gapSnapLine.points.map((p) =>
|
||||||
pointFrom(round(p[0]), round(p[1])),
|
point(round(p[0]), round(p[1])),
|
||||||
) as PointPair,
|
) as PointPair,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -1144,35 +1120,35 @@ export const snapResizingElements = (
|
|||||||
if (transformHandle) {
|
if (transformHandle) {
|
||||||
switch (transformHandle) {
|
switch (transformHandle) {
|
||||||
case "e": {
|
case "e": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY));
|
selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "w": {
|
case "w": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY));
|
selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "n": {
|
case "n": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY));
|
selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "s": {
|
case "s": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY));
|
selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ne": {
|
case "ne": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, minY));
|
selectionSnapPoints.push(point(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "nw": {
|
case "nw": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY));
|
selectionSnapPoints.push(point(minX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "se": {
|
case "se": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, maxY));
|
selectionSnapPoints.push(point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sw": {
|
case "sw": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, maxY));
|
selectionSnapPoints.push(point(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1215,10 +1191,10 @@ export const snapResizingElements = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const corners: GlobalPoint[] = [
|
const corners: GlobalPoint[] = [
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
pointFrom(x1, y2),
|
point(x1, y2),
|
||||||
pointFrom(x2, y1),
|
point(x2, y1),
|
||||||
pointFrom(x2, y2),
|
point(x2, y2),
|
||||||
];
|
];
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
@@ -1255,7 +1231,7 @@ export const snapNewElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: GlobalPoint[] = [
|
const selectionSnapPoints: GlobalPoint[] = [
|
||||||
pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
point(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
||||||
];
|
];
|
||||||
|
|
||||||
const snapDistance = getSnapDistance(app.state.zoom.value);
|
const snapDistance = getSnapDistance(app.state.zoom.value);
|
||||||
@@ -1355,7 +1331,7 @@ export const getSnapLinesAtPointer = (
|
|||||||
|
|
||||||
verticalSnapLines.push({
|
verticalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, pointFrom(corner[0], pointer.y)],
|
points: [corner, point(corner[0], pointer.y)],
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1371,7 +1347,7 @@ export const getSnapLinesAtPointer = (
|
|||||||
|
|
||||||
horizontalSnapLines.push({
|
horizontalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, pointFrom(pointer.x, corner[1])],
|
points: [corner, point(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 { pointFrom } from "../../math";
|
import { point } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -32,12 +32,7 @@ describe("element binding", () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 1,
|
height: 1,
|
||||||
points: [
|
points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)],
|
||||||
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);
|
||||||
@@ -315,7 +310,7 @@ describe("element binding", () => {
|
|||||||
const arrow1 = API.createElement({
|
const arrow1 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
@@ -333,7 +328,7 @@ describe("element binding", () => {
|
|||||||
const arrow2 = API.createElement({
|
const arrow2 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow2",
|
id: "arrow2",
|
||||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "text1",
|
elementId: "text1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
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 { pointFrom, type Radians } from "../../math";
|
import { point, 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: [
|
||||||
pointFrom<LocalPoint>(0, 0),
|
point<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
point<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
||||||
pointFrom<LocalPoint>(828.0126953125, 410.51605224609375),
|
point<LocalPoint>(828.0126953125, 410.51605224609375),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,18 +20,7 @@ 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,
|
||||||
@@ -49,7 +38,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 { pointFrom, type LocalPoint, type Radians } from "../../../math";
|
import { point, 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.
|
||||||
@@ -58,19 +47,6 @@ 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);
|
||||||
@@ -199,8 +175,6 @@ 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"]
|
||||||
@@ -240,14 +214,6 @@ 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"
|
||||||
@@ -262,7 +228,6 @@ export class API {
|
|||||||
| "link"
|
| "link"
|
||||||
| "updated"
|
| "updated"
|
||||||
> = {
|
> = {
|
||||||
...custom,
|
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
frameId: rest.frameId ?? null,
|
frameId: rest.frameId ?? null,
|
||||||
@@ -342,8 +307,8 @@ export class API {
|
|||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
pointFrom<LocalPoint>(0, 0),
|
point<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(100, 100),
|
point<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
elbowed: rest.elbowed ?? false,
|
elbowed: rest.elbowed ?? false,
|
||||||
});
|
});
|
||||||
@@ -355,8 +320,8 @@ export class API {
|
|||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
pointFrom<LocalPoint>(0, 0),
|
point<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(100, 100),
|
point<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"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 { pointFrom, pointRotateRads } from "../../../math";
|
import { point, 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 = pointFrom<GlobalPoint>(
|
const target = point<GlobalPoint>(
|
||||||
x +
|
x +
|
||||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||||
y,
|
y,
|
||||||
@@ -151,12 +151,9 @@ const getElementPointForSelection = (
|
|||||||
|
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element)) {
|
||||||
const bounds = getElementPointsCoords(element, element.points);
|
const bounds = getElementPointsCoords(element, element.points);
|
||||||
center = pointFrom(
|
center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2);
|
||||||
(bounds[0] + bounds[2]) / 2,
|
|
||||||
(bounds[1] + bounds[3]) / 2,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
center = pointFrom(x + width / 2, y + height / 2);
|
center = point(x + width / 2, y + height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
@@ -472,8 +469,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 ?? [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(width, height),
|
point(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 { pointFrom } from "../../math";
|
import { point } 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: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(178.9000000000001, 0),
|
point(178.9000000000001, 0),
|
||||||
pointFrom(178.9000000000001, 236.10000000000002),
|
point(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: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(5, 5),
|
point(5, 5),
|
||||||
pointFrom(10, 10),
|
point(10, 10),
|
||||||
pointFrom(15, 15),
|
point(15, 15),
|
||||||
pointFrom(20, 20),
|
point(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, pointFrom } from "../../math";
|
import { pointCenter, point } 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 = pointFrom<GlobalPoint>(20, 20);
|
const p1 = point<GlobalPoint>(20, 20);
|
||||||
const p2 = pointFrom<GlobalPoint>(60, 20);
|
const p2 = point<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: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])],
|
points: [point(0, 0), point(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: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(p3[0], p3[1]),
|
point(p3[0], p3[1]),
|
||||||
pointFrom(p2[0] - p1[0], p2[1] - p1[1]),
|
point(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, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
|
drag(midpoint, point(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, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, point(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, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, point(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, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
|
drag(midpoint, point(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, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, point(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 = pointFrom<GlobalPoint>(
|
const endPoint = point<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 = pointFrom<GlobalPoint>(55, 45);
|
const firstSegmentMidpoint = point<GlobalPoint>(55, 45);
|
||||||
const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40);
|
const lastSegmentMidpoint = point<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,
|
||||||
pointFrom(
|
point(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -426,10 +426,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
pointFrom(
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
lastSegmentMidpoint[0] + delta,
|
|
||||||
lastSegmentMidpoint[1] + delta,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
@@ -478,10 +475,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -519,10 +516,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -559,7 +556,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// dragging line from last segment midpoint
|
// dragging line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
||||||
);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
@@ -592,11 +589,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 = pointFrom<GlobalPoint>(
|
const firstSegmentMidpoint = point<GlobalPoint>(
|
||||||
55.9697848965255,
|
55.9697848965255,
|
||||||
47.442326230998205,
|
47.442326230998205,
|
||||||
);
|
);
|
||||||
const lastSegmentMidpoint = pointFrom<GlobalPoint>(
|
const lastSegmentMidpoint = point<GlobalPoint>(
|
||||||
76.08587175006699,
|
76.08587175006699,
|
||||||
43.294165939653226,
|
43.294165939653226,
|
||||||
);
|
);
|
||||||
@@ -615,7 +612,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from first segment midpoint
|
// drag line from first segment midpoint
|
||||||
drag(
|
drag(
|
||||||
firstSegmentMidpoint,
|
firstSegmentMidpoint,
|
||||||
pointFrom(
|
point(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -625,10 +622,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
pointFrom(
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
lastSegmentMidpoint[0] + delta,
|
|
||||||
lastSegmentMidpoint[1] + delta,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
@@ -675,10 +669,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@@ -723,10 +717,10 @@ describe("Test Linear Elements", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
@@ -757,10 +751,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
drag(
|
drag(
|
||||||
lastSegmentMidpoint,
|
lastSegmentMidpoint,
|
||||||
pointFrom(
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
lastSegmentMidpoint[0] + delta,
|
|
||||||
lastSegmentMidpoint[1] + delta,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
@@ -820,8 +811,8 @@ describe("Test Linear Elements", () => {
|
|||||||
API.setSelectedElements([line]);
|
API.setSelectedElements([line]);
|
||||||
enterLineEditingMode(line, true);
|
enterLineEditingMode(line, true);
|
||||||
drag(
|
drag(
|
||||||
pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
point(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
||||||
pointFrom(
|
point(
|
||||||
dragEndPositionOffset[0] + line.x,
|
dragEndPositionOffset[0] + line.x,
|
||||||
dragEndPositionOffset[1] + line.y,
|
dragEndPositionOffset[1] + line.y,
|
||||||
),
|
),
|
||||||
@@ -936,14 +927,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 = pointFrom<GlobalPoint>(
|
const firstSegmentMidpoint = point<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,
|
||||||
pointFrom(
|
point(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint[0] + delta,
|
||||||
firstSegmentMidpoint[1] + delta,
|
firstSegmentMidpoint[1] + delta,
|
||||||
),
|
),
|
||||||
@@ -1160,7 +1151,7 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Drag from last point
|
// Drag from last point
|
||||||
drag(points[1], pointFrom(points[1][0] + 300, points[1][1]));
|
drag(points[1], point(points[1][0] + 300, points[1][1]));
|
||||||
|
|
||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
@@ -1359,11 +1350,11 @@ describe("Test Linear Elements", () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
point: point(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: line.points.length - 1,
|
index: line.points.length - 1,
|
||||||
point: pointFrom(
|
point: point(
|
||||||
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 { pointFrom } from "../../math";
|
import { point } from "../../math";
|
||||||
|
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -220,17 +220,12 @@ 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: [
|
line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)],
|
||||||
pointFrom(0, 0),
|
|
||||||
pointFrom(60, -20),
|
|
||||||
pointFrom(20, 40),
|
|
||||||
pointFrom(-40, 0),
|
|
||||||
],
|
|
||||||
freedraw: [
|
freedraw: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(-2.474600807561444, 41.021700699972),
|
point(-2.474600807561444, 41.021700699972),
|
||||||
pointFrom(3.6627956000014024, 47.84174560617245),
|
point(3.6627956000014024, 47.84174560617245),
|
||||||
pointFrom(40.495224145598115, 47.15909710753482),
|
point(40.495224145598115, 47.15909710753482),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,11 +293,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: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(40, 140),
|
point(40, 140),
|
||||||
pointFrom(80, 60), // label's anchor
|
point(80, 60), // label's anchor
|
||||||
pointFrom(180, 20),
|
point(180, 20),
|
||||||
pointFrom(200, 120),
|
point(200, 120),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const label = await UI.editText(arrow, "Hello");
|
const label = await UI.editText(arrow, "Hello");
|
||||||
@@ -752,24 +747,24 @@ describe("multiple selection", () => {
|
|||||||
x: 60,
|
x: 60,
|
||||||
y: 40,
|
y: 40,
|
||||||
points: [
|
points: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(-40, 40),
|
point(-40, 40),
|
||||||
pointFrom(-60, 0),
|
point(-60, 0),
|
||||||
pointFrom(0, -40),
|
point(0, -40),
|
||||||
pointFrom(40, 20),
|
point(40, 20),
|
||||||
pointFrom(0, 40),
|
point(0, 40),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const freedraw = UI.createElement("freedraw", {
|
const freedraw = UI.createElement("freedraw", {
|
||||||
x: 63.56072661326618,
|
x: 63.56072661326618,
|
||||||
y: 100,
|
y: 100,
|
||||||
points: [
|
points: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(-43.56072661326618, 18.15048126846341),
|
point(-43.56072661326618, 18.15048126846341),
|
||||||
pointFrom(-43.56072661326618, 29.041198460587566),
|
point(-43.56072661326618, 29.041198460587566),
|
||||||
pointFrom(-38.115368017204105, 42.652452795512204),
|
point(-38.115368017204105, 42.652452795512204),
|
||||||
pointFrom(-19.964886748740696, 66.24829266003775),
|
point(-19.964886748740696, 66.24829266003775),
|
||||||
pointFrom(19.056612930986716, 77.1390098521619),
|
point(19.056612930986716, 77.1390098521619),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1106,13 +1101,13 @@ describe("multiple selection", () => {
|
|||||||
x: 60,
|
x: 60,
|
||||||
y: 0,
|
y: 0,
|
||||||
points: [
|
points: [
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(-40, 40),
|
point(-40, 40),
|
||||||
pointFrom(-20, 60),
|
point(-20, 60),
|
||||||
pointFrom(20, 20),
|
point(20, 20),
|
||||||
pointFrom(40, 40),
|
point(40, 40),
|
||||||
pointFrom(-20, 100),
|
point(-20, 100),
|
||||||
pointFrom(-60, 60),
|
point(-60, 60),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,679 +0,0 @@
|
|||||||
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,12 +35,6 @@ 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";
|
||||||
@@ -277,10 +271,6 @@ 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
|
||||||
@@ -749,10 +739,6 @@ 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,9 +1,4 @@
|
|||||||
import {
|
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
|
||||||
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";
|
||||||
@@ -57,8 +52,8 @@ export const debugDrawPoint = (
|
|||||||
|
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
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),
|
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
@@ -67,8 +62,8 @@ export const debugDrawPoint = (
|
|||||||
);
|
);
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
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),
|
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
@@ -88,20 +83,20 @@ export const debugDrawBoundingBox = (
|
|||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
@@ -123,20 +118,20 @@ export const debugDrawBounds = (
|
|||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
),
|
),
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isPointOnSymmetricArc } from "./arc";
|
import { isPointOnSymmetricArc } from "./arc";
|
||||||
import { pointFrom } from "./point";
|
import { point } 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,
|
||||||
},
|
},
|
||||||
pointFrom(0.92291667, 0.385),
|
point(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,
|
||||||
},
|
},
|
||||||
pointFrom(-0.92291667, 0.385),
|
point(-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,
|
||||||
},
|
},
|
||||||
pointFrom(-0.5, 0.5),
|
point(-0.5, 0.5),
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
import { pointFrom, pointRotateRads } from "./point";
|
import { point, 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(
|
||||||
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||||
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
point(pointsIn[1][0], pointsIn[1][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
|
point(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(pointFrom(points[0][0], points[0][1]));
|
out.push(point(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] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
|
b[0] = point(cachedVertArray[0], cachedVertArray[1]);
|
||||||
b[1] = pointFrom(
|
b[1] = point(
|
||||||
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] = pointFrom(
|
b[2] = point(
|
||||||
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] = pointFrom(points[i + 1][0], points[i + 1][1]);
|
b[3] = point(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 pointFrom(x, y);
|
return point(x, y);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pointFrom, pointRotateRads } from "./point";
|
import { point, 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(
|
||||||
pointFrom(x1, y1),
|
point(x1, y1),
|
||||||
pointFrom(x2, y2),
|
point(x2, y2),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||||
const res2 = pointRotateRads(
|
const res2 = pointRotateRads(
|
||||||
pointFrom(rotatedX, rotatedY),
|
point(rotatedX, rotatedY),
|
||||||
pointFrom(x2, y2),
|
point(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 pointFrom<Point extends GlobalPoint | LocalPoint>(
|
export function point<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
|
||||||
? pointFrom<Point>(numberArray[0], numberArray[1])
|
? point<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 pointFrom(
|
return point(
|
||||||
(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 pointFrom(p[0] + v[0], p[1] + v[1]);
|
return point(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 pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
return point((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 pointFrom(a[0] + b[0], a[1] + b[1]);
|
return point(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 pointFrom(a[0] - b[0], a[1] - b[1]);
|
return point(a[0] - b[0], a[1] - b[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentRotate,
|
lineSegmentRotate,
|
||||||
pointFrom,
|
point,
|
||||||
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(
|
||||||
pointFrom(1.4, 1.65),
|
point(1.4, 1.65),
|
||||||
pointFrom(1.9, 7.9),
|
point(1.9, 7.9),
|
||||||
pointFrom(5.9, 1.65),
|
point(5.9, 1.65),
|
||||||
pointFrom(6.44, 4.84),
|
point(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(pointFrom(2, 4), c, 0.1)).toBe(true);
|
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
|
||||||
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
|
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
|
||||||
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
|
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
|
expect(pointOnCurve(point(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(pointFrom(1, 0), pointFrom(1, 2)),
|
lineSegment(point(1, 0), point(1, 2)),
|
||||||
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
|
lineSegment(point(1, 2), point(2, 2)),
|
||||||
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
|
lineSegment(point(2, 2), point(2, 1)),
|
||||||
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
|
lineSegment(point(2, 1), point(3, 1)),
|
||||||
];
|
];
|
||||||
|
|
||||||
it("point on the line", () => {
|
it("point on the line", () => {
|
||||||
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
|
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
|
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
|
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
|
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
|
||||||
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
|
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
|
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
|
||||||
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
|
expect(pointOnPolyline(point(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 = [
|
||||||
pointFrom(1, 0),
|
point(1, 0),
|
||||||
pointFrom(1, 2),
|
point(1, 2),
|
||||||
pointFrom(2, 2),
|
point(2, 2),
|
||||||
pointFrom(2, 1),
|
point(2, 1),
|
||||||
pointFrom(3, 1),
|
point(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, pointFrom(0, 0), rotation);
|
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||||
const rotatedPolyline = polyline.map((line) =>
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||||
);
|
);
|
||||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
|
const falsePoints = [point(0, 1), point(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, pointFrom(0, 0), rotation);
|
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||||
const rotatedPolyline = polyline.map((line) =>
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
lineSegmentRotate(line, degreesToRadians(rotation), point(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,
|
||||||
pointFrom,
|
point,
|
||||||
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 = pointFrom(equation(t, 0), equation(t, 1));
|
const nextPoint: Point = point(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 {
|
||||||
pointFrom,
|
point,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
polygon,
|
polygon,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
@@ -23,127 +23,93 @@ 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(
|
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||||
pointFrom(1, 0),
|
|
||||||
pointFrom(1, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
it("point on the line", () => {
|
it("point on the line", () => {
|
||||||
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
|
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
|
||||||
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
|
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
|
||||||
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
|
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and polygon", () => {
|
describe("point and polygon", () => {
|
||||||
const poly: Polygon<GlobalPoint> = polygon(
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
pointFrom(10, 10),
|
point(10, 10),
|
||||||
pointFrom(50, 10),
|
point(50, 10),
|
||||||
pointFrom(50, 50),
|
point(50, 50),
|
||||||
pointFrom(10, 50),
|
point(10, 50),
|
||||||
);
|
);
|
||||||
|
|
||||||
it("point on polygon", () => {
|
it("point on polygon", () => {
|
||||||
expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true);
|
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true);
|
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true);
|
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true);
|
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false);
|
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
|
||||||
expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false);
|
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point in polygon", () => {
|
it("point in polygon", () => {
|
||||||
const poly: Polygon<GlobalPoint> = polygon(
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
pointFrom(0, 0),
|
point(0, 0),
|
||||||
pointFrom(2, 0),
|
point(2, 0),
|
||||||
pointFrom(2, 2),
|
point(2, 2),
|
||||||
pointFrom(0, 2),
|
point(0, 2),
|
||||||
);
|
);
|
||||||
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
|
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
|
||||||
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
|
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and ellipse", () => {
|
describe("point and ellipse", () => {
|
||||||
const ellipse: Ellipse<GlobalPoint> = {
|
const ellipse: Ellipse<GlobalPoint> = {
|
||||||
center: pointFrom(0, 0),
|
center: point(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(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
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);
|
expect(pointOnEllipse(point(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(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, -0.8), ellipse)).toBe(true);
|
||||||
|
|
||||||
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
|
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
||||||
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("line and line", () => {
|
describe("line and line", () => {
|
||||||
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
||||||
pointFrom(1, 4),
|
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
||||||
pointFrom(3, 4),
|
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
);
|
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
const lineB: LineSegment<GlobalPoint> = lineSegment(
|
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
||||||
pointFrom(2, 1),
|
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
||||||
pointFrom(2, 7),
|
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
||||||
);
|
|
||||||
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]);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user