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 { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||
import { useMathSubtype } from "../packages/excalidraw/element/subtypes/mathjax";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
@@ -127,6 +126,8 @@ import DebugCanvas, {
|
||||
loadSavedDebugState,
|
||||
} from "./components/DebugCanvas";
|
||||
import { AIComponents } from "./components/AI";
|
||||
import type { SaveWarningRef } from "./components/SaveWarning";
|
||||
import { SaveWarning } from "./components/SaveWarning";
|
||||
|
||||
polyfill();
|
||||
|
||||
@@ -332,6 +333,8 @@ const ExcalidrawWrapper = () => {
|
||||
|
||||
const [langCode, setLangCode] = useAppLangCode();
|
||||
|
||||
const activityRef = useRef<SaveWarningRef | null>(null);
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -356,8 +359,6 @@ const ExcalidrawWrapper = () => {
|
||||
const [excalidrawAPI, excalidrawRefCallback] =
|
||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||
|
||||
useMathSubtype(excalidrawAPI);
|
||||
|
||||
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
||||
const [collabAPI] = useAtom(collabAPIAtom);
|
||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||
@@ -618,6 +619,8 @@ const ExcalidrawWrapper = () => {
|
||||
collabAPI.syncElements(elements);
|
||||
}
|
||||
|
||||
activityRef.current?.activity();
|
||||
|
||||
// this check is redundant, but since this is a hot path, it's best
|
||||
// not to evaludate the nested expression every time
|
||||
if (!LocalData.isSavePaused()) {
|
||||
@@ -859,6 +862,7 @@ const ExcalidrawWrapper = () => {
|
||||
setTheme={(theme) => setAppTheme(theme)}
|
||||
refresh={() => forceRefresh((prev) => !prev)}
|
||||
/>
|
||||
<SaveWarning ref={activityRef} />
|
||||
<AppWelcomeScreen
|
||||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
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>
|
||||
<% } %>
|
||||
|
||||
<!-- 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 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
||||
@@ -18,6 +18,43 @@
|
||||
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 {
|
||||
border-radius: var(--space-factor);
|
||||
color: var(--color-primary);
|
||||
|
||||
@@ -48,8 +48,6 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
// don't auto-inline small assets (i.e. fonts hosted on CDN)
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
plugins: [
|
||||
woff2BrowserPlugin(),
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
"husky": "7.0.4",
|
||||
"jsdom": "22.1.0",
|
||||
"lint-staged": "12.3.7",
|
||||
"patch-package": "8.0.0",
|
||||
"pepjs": "0.5.3",
|
||||
"postinstall-postinstall": "2.1.0",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"typescript": "4.9.4",
|
||||
@@ -63,7 +61,6 @@
|
||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "patch-package",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "yarn --cwd ./excalidraw-app start",
|
||||
"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
|
||||
|
||||
- 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)
|
||||
|
||||
- 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)
|
||||
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
computeBoundTextPosition,
|
||||
computeContainerDimensionForBoundText,
|
||||
getBoundTextElement,
|
||||
measureTextElement,
|
||||
measureText,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ import type {
|
||||
} from "../element/types";
|
||||
import type { AppState } from "../types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { arrayToMap, getFontString } from "../utils";
|
||||
import { register } from "./register";
|
||||
import { syncMovedIndices } from "../fractionalIndex";
|
||||
import { StoreAction } from "../store";
|
||||
@@ -51,9 +51,11 @@ export const actionUnbindText = register({
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
const { width, height } = measureTextElement(boundTextElement, {
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
const { width, height } = measureText(
|
||||
boundTextElement.originalText,
|
||||
getFontString(boundTextElement),
|
||||
boundTextElement.lineHeight,
|
||||
);
|
||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||
element.id,
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||
import type { AppState } from "../types";
|
||||
import { resetCursor } from "../cursor";
|
||||
import { StoreAction } from "../store";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
import { isPathALoop } from "../shapes";
|
||||
|
||||
export const actionFinalize = register({
|
||||
@@ -115,7 +115,7 @@ export const actionFinalize = register({
|
||||
mutateElement(multiPointElement, {
|
||||
points: linePoints.map((p, index) =>
|
||||
index === linePoints.length - 1
|
||||
? pointFrom(firstPoint[0], firstPoint[1])
|
||||
? point(firstPoint[0], firstPoint[1])
|
||||
: p,
|
||||
),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Excalidraw } from "../index";
|
||||
import { render } from "../tests/test-utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||
|
||||
const { h } = window;
|
||||
@@ -50,11 +50,11 @@ describe("flipping re-centers selection", () => {
|
||||
startArrowhead: null,
|
||||
endArrowhead: "arrow",
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, -35),
|
||||
pointFrom(-90.9, -35),
|
||||
pointFrom(-90.9, 204.9),
|
||||
pointFrom(65.1, 204.9),
|
||||
point(0, 0),
|
||||
point(0, -35),
|
||||
point(-90.9, -35),
|
||||
point(-90.9, 204.9),
|
||||
point(65.1, 204.9),
|
||||
],
|
||||
elbowed: true,
|
||||
}),
|
||||
|
||||
@@ -116,7 +116,7 @@ import {
|
||||
import { mutateElbowArrow } from "../element/routing";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom, vector } from "../../math";
|
||||
import { point, vector } from "../../math";
|
||||
|
||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||
|
||||
@@ -1651,7 +1651,7 @@ export const actionChangeArrowType = register({
|
||||
elementsMap,
|
||||
[finalStartPoint, finalEndPoint].map(
|
||||
(p): LocalPoint =>
|
||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||
point(p[0] - newElement.x, p[1] - newElement.y),
|
||||
),
|
||||
vector(0, 0),
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
ActionResult,
|
||||
PanelComponentProps,
|
||||
ActionSource,
|
||||
ActionPredicateFn,
|
||||
} from "./types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
@@ -46,7 +45,6 @@ const trackAction = (
|
||||
|
||||
export class ActionManager {
|
||||
actions = {} as Record<ActionName, Action>;
|
||||
actionPredicates = [] as ActionPredicateFn[];
|
||||
|
||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||
|
||||
@@ -74,37 +72,6 @@ export class ActionManager {
|
||||
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) {
|
||||
this.actions[action.name] = action;
|
||||
}
|
||||
@@ -121,7 +88,7 @@ export class ActionManager {
|
||||
(action) =>
|
||||
(action.name in canvasActions
|
||||
? canvasActions[action.name as keyof typeof canvasActions]
|
||||
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||
: true) &&
|
||||
action.keyTest &&
|
||||
action.keyTest(
|
||||
event,
|
||||
@@ -180,7 +147,7 @@ export class ActionManager {
|
||||
"PanelComponent" in this.actions[name] &&
|
||||
(name in canvasActions
|
||||
? canvasActions[name as keyof typeof canvasActions]
|
||||
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||
: true)
|
||||
) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
@@ -202,7 +169,6 @@ export class ActionManager {
|
||||
|
||||
return (
|
||||
<PanelComponent
|
||||
key={name}
|
||||
elements={this.getElementsIncludingDeleted()}
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
@@ -216,31 +182,13 @@ export class ActionManager {
|
||||
return null;
|
||||
};
|
||||
|
||||
isActionEnabled = (
|
||||
action: Action,
|
||||
opts?: {
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
data?: Record<string, any>;
|
||||
noPredicates?: boolean;
|
||||
},
|
||||
): boolean => {
|
||||
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||
isActionEnabled = (action: Action) => {
|
||||
const elements = this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
const data = opts?.data;
|
||||
|
||||
if (
|
||||
!opts?.noPredicates &&
|
||||
action.predicate &&
|
||||
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let enabled = true;
|
||||
this.actionPredicates.forEach((fn) => {
|
||||
if (!fn(action, elements, appState, this.app, data)) {
|
||||
enabled = false;
|
||||
}
|
||||
});
|
||||
return enabled;
|
||||
return (
|
||||
!action.predicate ||
|
||||
action.predicate(elements, appState, this.app.props, this.app)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { isDarwin } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import type { SubtypeOf } from "../utility-types";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import type { ActionName, CustomActionName } from "./types";
|
||||
import type { ActionName } from "./types";
|
||||
|
||||
export type ShortcutName =
|
||||
| SubtypeOf<
|
||||
ActionName,
|
||||
| CustomActionName
|
||||
| "toggleTheme"
|
||||
| "loadScene"
|
||||
| "clearCanvas"
|
||||
@@ -55,15 +54,6 @@ export type ShortcutName =
|
||||
| "commandPalette"
|
||||
| "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[]> = {
|
||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||
|
||||
@@ -41,24 +41,10 @@ type ActionFn = (
|
||||
app: AppClassProperties,
|
||||
) => 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 ActionFilterFn = (action: Action) => void;
|
||||
|
||||
export const makeCustomActionName = (name: string) =>
|
||||
`custom.${name}` as CustomActionName;
|
||||
export type CustomActionName = `custom.${string}`;
|
||||
export type ActionName =
|
||||
| CustomActionName
|
||||
| "copy"
|
||||
| "cut"
|
||||
| "paste"
|
||||
@@ -193,7 +179,6 @@ export interface Action {
|
||||
appState: AppState,
|
||||
appProps: ExcalidrawProps,
|
||||
app: AppClassProperties,
|
||||
data?: Record<string, any>,
|
||||
) => boolean;
|
||||
checked?: (appState: Readonly<AppState>) => boolean;
|
||||
trackEvent:
|
||||
|
||||
@@ -170,8 +170,6 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
editingGroupId: { browser: true, export: false, server: false },
|
||||
editingLinearElement: { browser: false, 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 },
|
||||
penDetected: { browser: true, export: false, server: false },
|
||||
errorMessage: { browser: false, export: false, server: false },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Radians } from "../math";
|
||||
import { pointFrom } from "../math";
|
||||
import { point } from "../math";
|
||||
import {
|
||||
COLOR_PALETTE,
|
||||
DEFAULT_CHART_COLOR_INDEX,
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
import type { NonDeletedExcalidrawElement } from "./element/types";
|
||||
import { randomId } from "./random";
|
||||
import type { AppState } from "./types";
|
||||
import { selectSubtype } from "./element/subtypes";
|
||||
|
||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
@@ -27,8 +25,6 @@ export interface Spreadsheet {
|
||||
title: string | null;
|
||||
labels: string[] | null;
|
||||
values: number[];
|
||||
activeSubtypes?: AppState["activeSubtypes"];
|
||||
customData?: AppState["customData"];
|
||||
}
|
||||
|
||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||
@@ -199,17 +195,13 @@ const chartXLabels = (
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const custom = selectSubtype(spreadsheet, "text");
|
||||
return (
|
||||
spreadsheet.labels?.map((label, index) => {
|
||||
return newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
text:
|
||||
label.length > 8 && custom.subtype === undefined
|
||||
? `${label.slice(0, 5)}...`
|
||||
: label,
|
||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||
y: y + BAR_GAP / 2,
|
||||
width: BAR_WIDTH,
|
||||
@@ -217,7 +209,6 @@ const chartXLabels = (
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
verticalAlign: "top",
|
||||
...custom,
|
||||
});
|
||||
}) || []
|
||||
);
|
||||
@@ -238,7 +229,6 @@ const chartYLabels = (
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
@@ -249,7 +239,6 @@ const chartYLabels = (
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||
textAlign: "right",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
});
|
||||
|
||||
return [minYLabel, maxYLabel];
|
||||
@@ -271,8 +260,7 @@ const chartLines = (
|
||||
x,
|
||||
y,
|
||||
width: chartWidth,
|
||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
points: [point(0, 0), point(chartWidth, 0)],
|
||||
});
|
||||
|
||||
const yLine = newLinearElement({
|
||||
@@ -283,8 +271,7 @@ const chartLines = (
|
||||
x,
|
||||
y,
|
||||
height: chartHeight,
|
||||
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
points: [point(0, 0), point(0, -chartHeight)],
|
||||
});
|
||||
|
||||
const maxLine = newLinearElement({
|
||||
@@ -297,8 +284,7 @@ const chartLines = (
|
||||
strokeStyle: "dotted",
|
||||
width: chartWidth,
|
||||
opacity: GRID_OPACITY,
|
||||
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
points: [point(0, 0), point(chartWidth, 0)],
|
||||
});
|
||||
|
||||
return [xLine, yLine, maxLine];
|
||||
@@ -325,7 +311,6 @@ const chartBaseElements = (
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||
roundness: null,
|
||||
textAlign: "center",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -342,7 +327,6 @@ const chartBaseElements = (
|
||||
strokeColor: COLOR_PALETTE.black,
|
||||
fillStyle: "solid",
|
||||
opacity: 6,
|
||||
...selectSubtype(spreadsheet, "rectangle"),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -375,7 +359,6 @@ const chartTypeBar = (
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
...selectSubtype(spreadsheet, "rectangle"),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -426,7 +409,6 @@ const chartTypeLine = (
|
||||
width: maxX - minX,
|
||||
strokeWidth: 2,
|
||||
points: points as any,
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
|
||||
const dots = spreadsheet.values.map((value, index) => {
|
||||
@@ -443,7 +425,6 @@ const chartTypeLine = (
|
||||
y: y + cy - BAR_GAP * 2,
|
||||
width: BAR_GAP,
|
||||
height: BAR_GAP,
|
||||
...selectSubtype(spreadsheet, "ellipse"),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -460,8 +441,7 @@ const chartTypeLine = (
|
||||
height: cy,
|
||||
strokeStyle: "dotted",
|
||||
opacity: GRID_OPACITY,
|
||||
points: [pointFrom(0, 0), pointFrom(0, cy)],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
points: [point(0, 0), point(0, cy)],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import type { AppState, BinaryFiles } from "./types";
|
||||
import type { BinaryFiles } from "./types";
|
||||
import type { Spreadsheet } from "./charts";
|
||||
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
import {
|
||||
@@ -333,7 +333,6 @@ const parseClipboardEvent = async (
|
||||
export const parseClipboard = async (
|
||||
event: ClipboardEvent,
|
||||
isPlainPaste = false,
|
||||
appState?: AppState,
|
||||
): Promise<ClipboardData> => {
|
||||
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
||||
|
||||
@@ -350,10 +349,6 @@ export const parseClipboard = async (
|
||||
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
||||
|
||||
if (spreadsheetResult) {
|
||||
if ("spreadsheet" in spreadsheetResult) {
|
||||
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
|
||||
spreadsheetResult.spreadsheet.customData = appState?.customData;
|
||||
}
|
||||
return spreadsheetResult;
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { SubtypeShapeActions } from "./Subtypes";
|
||||
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import {
|
||||
@@ -137,7 +136,6 @@ export const SelectedShapeActions = ({
|
||||
{canChangeBackgroundColor(appState, targetElements) && (
|
||||
<div>{renderAction("changeBackgroundColor")}</div>
|
||||
)}
|
||||
<SubtypeShapeActions elements={targetElements} />
|
||||
{showFillIcons && renderAction("changeFillStyle")}
|
||||
|
||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||
|
||||
@@ -301,18 +301,6 @@ import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Toast } from "./Toast";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
import type {
|
||||
SubtypeLoadedCb,
|
||||
SubtypeRecord,
|
||||
SubtypePrepFn,
|
||||
} from "../element/subtypes";
|
||||
import {
|
||||
checkRefreshOnSubtypeLoad,
|
||||
isSubtypeAction,
|
||||
prepareSubtype,
|
||||
selectSubtype,
|
||||
subtypeActionPredicate,
|
||||
} from "../element/subtypes";
|
||||
import {
|
||||
dataURLToFile,
|
||||
generateIdFromFile,
|
||||
@@ -457,7 +445,7 @@ import {
|
||||
} from "../element/flowchart";
|
||||
import { searchItemInFocusAtom } from "./SearchMenu";
|
||||
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 AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@@ -722,7 +710,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
registerAction: (action: Action) => {
|
||||
this.actionManager.registerAction(action);
|
||||
},
|
||||
addSubtype: this.addSubtype,
|
||||
refresh: this.refresh,
|
||||
setToast: this.setToast,
|
||||
id: this.id,
|
||||
@@ -759,19 +746,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.actionManager.registerAction(
|
||||
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) {
|
||||
@@ -2977,7 +2951,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// event else some browsers (FF...) will clear the clipboardData
|
||||
// (something something security)
|
||||
let file = event?.clipboardData?.files[0];
|
||||
const data = await parseClipboard(event, isPlainPaste, this.state);
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
if (!file && !isPlainPaste) {
|
||||
if (data.mixedContent) {
|
||||
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
||||
@@ -3415,7 +3389,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: DEFAULT_TEXT_ALIGN,
|
||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||
...selectSubtype(this.state, "text"),
|
||||
locked: false,
|
||||
};
|
||||
const fontString = getFontString({
|
||||
@@ -4937,7 +4910,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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
|
||||
@@ -5125,7 +5098,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
...selectSubtype(this.state, "text"),
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
@@ -5297,7 +5269,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
this.device.editor.isMobile,
|
||||
)
|
||||
);
|
||||
@@ -5309,14 +5281,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isTouchScreen: boolean,
|
||||
) => {
|
||||
const draggedDistance = pointDistance(
|
||||
pointFrom(
|
||||
point(
|
||||
this.lastPointerDownEvent!.clientX,
|
||||
this.lastPointerDownEvent!.clientY,
|
||||
),
|
||||
pointFrom(
|
||||
this.lastPointerUpEvent!.clientX,
|
||||
this.lastPointerUpEvent!.clientY,
|
||||
),
|
||||
point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY),
|
||||
);
|
||||
if (
|
||||
!this.hitLinkElement ||
|
||||
@@ -5335,7 +5304,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.hitLinkElement,
|
||||
elementsMap,
|
||||
this.state,
|
||||
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||
point(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||
this.device.editor.isMobile,
|
||||
);
|
||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||
@@ -5346,7 +5315,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.hitLinkElement,
|
||||
elementsMap,
|
||||
this.state,
|
||||
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||
point(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||
this.device.editor.isMobile,
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||
@@ -5596,7 +5565,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// threshold, add a point
|
||||
if (
|
||||
pointDistance(
|
||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||
point(scenePointerX - rx, scenePointerY - ry),
|
||||
lastPoint,
|
||||
) >= LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
@@ -5605,7 +5574,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
{
|
||||
points: [
|
||||
...points,
|
||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||
point<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||
],
|
||||
},
|
||||
false,
|
||||
@@ -5619,7 +5588,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
points.length > 2 &&
|
||||
lastCommittedPoint &&
|
||||
pointDistance(
|
||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||
point(scenePointerX - rx, scenePointerY - ry),
|
||||
lastCommittedPoint,
|
||||
) < LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
@@ -5667,7 +5636,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
[
|
||||
...points.slice(0, -1),
|
||||
pointFrom<LocalPoint>(
|
||||
point<LocalPoint>(
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
),
|
||||
@@ -5686,7 +5655,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
{
|
||||
points: [
|
||||
...points.slice(0, -1),
|
||||
pointFrom<LocalPoint>(
|
||||
point<LocalPoint>(
|
||||
lastCommittedX + dxFromLastCommitted,
|
||||
lastCommittedY + dyFromLastCommitted,
|
||||
),
|
||||
@@ -5915,8 +5884,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
const distance = pointDistance(
|
||||
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
const threshold = this.getElementHitThreshold();
|
||||
const p = { ...pointerDownState.lastCoords };
|
||||
@@ -6428,7 +6397,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.hitLinkElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
)
|
||||
) {
|
||||
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||
@@ -7119,7 +7088,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
simulatePressure,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
points: [pointFrom<LocalPoint>(0, 0)],
|
||||
points: [point<LocalPoint>(0, 0)],
|
||||
pressures: simulatePressure ? [] : [event.pressure],
|
||||
});
|
||||
|
||||
@@ -7282,7 +7251,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
roughness: this.state.currentItemRoughness,
|
||||
roundness: null,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
...selectSubtype(this.state, "image"),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
@@ -7329,10 +7297,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
multiElement.points.length > 1 &&
|
||||
lastCommittedPoint &&
|
||||
pointDistance(
|
||||
pointFrom(
|
||||
pointerDownState.origin.x - rx,
|
||||
pointerDownState.origin.y - ry,
|
||||
),
|
||||
point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry),
|
||||
lastCommittedPoint,
|
||||
) < LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
@@ -7399,7 +7364,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
...selectSubtype(this.state, elementType),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||
@@ -7419,7 +7383,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.currentItemRoundness === "round"
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: null,
|
||||
...selectSubtype(this.state, elementType),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
@@ -7436,7 +7399,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
});
|
||||
mutateElement(element, {
|
||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||
points: [...element.points, point<LocalPoint>(0, 0)],
|
||||
});
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
pointerDownState.origin,
|
||||
@@ -7500,7 +7463,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness: this.getCurrentItemRoundness(elementType),
|
||||
...selectSubtype(this.state, elementType),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
} as const;
|
||||
@@ -7690,8 +7652,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
if (
|
||||
pointDistance(
|
||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||
point(pointerCoords.x, pointerCoords.y),
|
||||
point(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||
) < DRAGGING_THRESHOLD
|
||||
) {
|
||||
return;
|
||||
@@ -8040,7 +8002,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
points: [...points, point<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
},
|
||||
false,
|
||||
@@ -8069,7 +8031,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
points: [...points, point<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
);
|
||||
@@ -8077,7 +8039,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
mutateElbowArrow(
|
||||
newElement,
|
||||
elementsMap,
|
||||
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
[...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||
vector(0, 0),
|
||||
undefined,
|
||||
{
|
||||
@@ -8089,7 +8051,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
);
|
||||
@@ -8398,9 +8360,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: [...newElement.pressures, childEvent.pressure];
|
||||
|
||||
mutateElement(newElement, {
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
points: [...points, point<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||
lastCommittedPoint: point<LocalPoint>(dx, dy),
|
||||
});
|
||||
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
@@ -8447,7 +8409,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
mutateElement(newElement, {
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
point<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
@@ -8761,8 +8723,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.eraserTrail.endPath();
|
||||
|
||||
const draggedDistance = pointDistance(
|
||||
pointFrom(pointerStart.clientX, pointerStart.clientY),
|
||||
pointFrom(pointerEnd.clientX, pointerEnd.clientY),
|
||||
point(pointerStart.clientX, pointerStart.clientY),
|
||||
point(pointerEnd.clientX, pointerEnd.clientY),
|
||||
);
|
||||
|
||||
if (draggedDistance === 0) {
|
||||
@@ -10080,39 +10042,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const elementsToHighlight = new Set<ExcalidrawElement>();
|
||||
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(
|
||||
this.scene.getNonDeletedElements(),
|
||||
frame,
|
||||
@@ -10133,29 +10062,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private getContextMenuItems = (
|
||||
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 => {
|
||||
const options: ContextMenuItems = [];
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import Scene from "../scene/Scene";
|
||||
import { SubtypeToggles } from "./Subtypes";
|
||||
import { LaserPointerButton } from "./LaserPointerButton";
|
||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||
import { Stats } from "./Stats";
|
||||
@@ -300,7 +299,6 @@ const LayerUI = ({
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
<SubtypeToggles />
|
||||
{isCollaborating && (
|
||||
<Island
|
||||
style={{
|
||||
|
||||
@@ -24,7 +24,6 @@ import { PenModeButton } from "./PenModeButton";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { SubtypeToggles } from "./Subtypes";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: UIAppState;
|
||||
@@ -90,7 +89,6 @@ export const MobileMenu = ({
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
<SubtypeToggles />
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
{!appState.viewModeEnabled && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import type { ChartElements, Spreadsheet } from "../charts";
|
||||
import { renderSpreadsheet } from "../charts";
|
||||
import type { ChartType, ElementsMap } from "../element/types";
|
||||
import type { ChartType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import type { UIAppState } from "../types";
|
||||
@@ -11,12 +11,6 @@ import { useApp } from "./App";
|
||||
import { Dialog } from "./Dialog";
|
||||
|
||||
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;
|
||||
|
||||
@@ -32,64 +26,41 @@ const ChartPreviewBtn = (props: {
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
setChartElements(elements);
|
||||
let svg: SVGSVGElement;
|
||||
const previewNode = previewRef.current!;
|
||||
|
||||
(async () => {
|
||||
(async () => {
|
||||
let elements: ChartElements;
|
||||
await ensureSubtypesLoaded(
|
||||
props.spreadsheet?.activeSubtypes ?? [],
|
||||
() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
svg = await exportToSvg(
|
||||
elements,
|
||||
{
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
},
|
||||
null, // files
|
||||
);
|
||||
svg.querySelector(".style-fonts")?.remove();
|
||||
previewNode.replaceChildren();
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const elementsMap = new Map() as ElementsMap;
|
||||
for (const element of elements) {
|
||||
if (!element.isDeleted) {
|
||||
elementsMap.set(element.id, element);
|
||||
}
|
||||
}
|
||||
elements.forEach(
|
||||
(el) =>
|
||||
isTextElement(el) &&
|
||||
redrawTextBoundingBox(
|
||||
el,
|
||||
getContainerElement(el, elementsMap),
|
||||
elementsMap,
|
||||
),
|
||||
);
|
||||
setChartElements(elements);
|
||||
},
|
||||
).then(async () => {
|
||||
svg = await exportToSvg(
|
||||
elements,
|
||||
{
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
},
|
||||
null, // files
|
||||
);
|
||||
svg.querySelector(".style-fonts")?.remove();
|
||||
previewNode.replaceChildren();
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
previewNode.replaceChildren();
|
||||
};
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
previewNode.replaceChildren();
|
||||
};
|
||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||
import { point, type GlobalPoint } from "../../../math";
|
||||
|
||||
interface MultiDimensionProps {
|
||||
property: "width" | "height";
|
||||
@@ -182,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
initialHeight,
|
||||
aspectRatio,
|
||||
pointFrom(x1, y1),
|
||||
point(x1, y1),
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
@@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
initialHeight,
|
||||
aspectRatio,
|
||||
pointFrom(x1, y1),
|
||||
point(x1, y1),
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useMemo } from "react";
|
||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type { AppState } from "../../types";
|
||||
import { pointFrom, pointRotateRads } from "../../../math";
|
||||
import { point, pointRotateRads } from "../../../math";
|
||||
|
||||
interface MultiPositionProps {
|
||||
property: "x" | "y";
|
||||
@@ -44,8 +44,8 @@ const moveElements = (
|
||||
origElement.y + origElement.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(origElement.x, origElement.y),
|
||||
pointFrom(cx, cy),
|
||||
point(origElement.x, origElement.y),
|
||||
point(cx, cy),
|
||||
origElement.angle,
|
||||
);
|
||||
|
||||
@@ -97,8 +97,8 @@ const moveGroupTo = (
|
||||
];
|
||||
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(latestElement.x, latestElement.y),
|
||||
pointFrom(cx, cy),
|
||||
point(latestElement.x, latestElement.y),
|
||||
point(cx, cy),
|
||||
latestElement.angle,
|
||||
);
|
||||
|
||||
@@ -171,8 +171,8 @@ const handlePositionChange: DragInputCallbackType<
|
||||
origElement.y + origElement.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(origElement.x, origElement.y),
|
||||
pointFrom(cx, cy),
|
||||
point(origElement.x, origElement.y),
|
||||
point(cx, cy),
|
||||
origElement.angle,
|
||||
);
|
||||
|
||||
@@ -241,8 +241,8 @@ const MultiPosition = ({
|
||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
||||
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(el.x, el.y),
|
||||
pointFrom(cx, cy),
|
||||
point(el.x, el.y),
|
||||
point(cx, cy),
|
||||
el.angle,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { DragInputCallbackType } from "./DragInput";
|
||||
import { getStepSizedValue, moveElement } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
import { pointFrom, pointRotateRads } from "../../../math";
|
||||
import { point, pointRotateRads } from "../../../math";
|
||||
|
||||
interface PositionProps {
|
||||
property: "x" | "y";
|
||||
@@ -33,8 +33,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
origElement.y + origElement.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(origElement.x, origElement.y),
|
||||
pointFrom(cx, cy),
|
||||
point(origElement.x, origElement.y),
|
||||
point(cx, cy),
|
||||
origElement.angle,
|
||||
);
|
||||
|
||||
@@ -93,8 +93,8 @@ const Position = ({
|
||||
appState,
|
||||
}: PositionProps) => {
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(element.x, element.y),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
point(element.x, element.y),
|
||||
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||
element.angle,
|
||||
);
|
||||
const value =
|
||||
|
||||
@@ -25,7 +25,7 @@ import { API } from "../../tests/helpers/api";
|
||||
import { actionGroup } from "../../actions";
|
||||
import { isInGroup } from "../../groups";
|
||||
import type { Degrees } from "../../../math";
|
||||
import { degreesToRadians, pointFrom, pointRotateRads } from "../../../math";
|
||||
import { degreesToRadians, point, pointRotateRads } from "../../../math";
|
||||
|
||||
const { h } = window;
|
||||
const mouse = new Pointer("mouse");
|
||||
@@ -264,8 +264,8 @@ describe("stats for a generic element", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
|
||||
@@ -283,8 +283,8 @@ describe("stats for a generic element", () => {
|
||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
||||
|
||||
let [newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
|
||||
@@ -294,8 +294,8 @@ describe("stats for a generic element", () => {
|
||||
testInputProperty(rectangle, "angle", "A", 45, 66);
|
||||
|
||||
[newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
||||
@@ -311,8 +311,8 @@ describe("stats for a generic element", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
||||
@@ -321,8 +321,8 @@ describe("stats for a generic element", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
];
|
||||
let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
||||
@@ -334,8 +334,8 @@ describe("stats for a generic element", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
];
|
||||
[currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||
pointFrom(rectangle.x, rectangle.y),
|
||||
pointFrom(cx, cy),
|
||||
point(rectangle.x, rectangle.y),
|
||||
point(cx, cy),
|
||||
rectangle.angle,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Radians } from "../../../math";
|
||||
import { pointFrom, pointRotateRads } from "../../../math";
|
||||
import { point, pointRotateRads } from "../../../math";
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
updateBoundElements,
|
||||
@@ -231,8 +231,8 @@ export const moveElement = (
|
||||
originalElement.y + originalElement.height / 2,
|
||||
];
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(originalElement.x, originalElement.y),
|
||||
pointFrom(cx, cy),
|
||||
point(originalElement.x, originalElement.y),
|
||||
point(cx, cy),
|
||||
originalElement.angle,
|
||||
);
|
||||
|
||||
@@ -240,8 +240,8 @@ export const moveElement = (
|
||||
const changeInY = newTopLeftY - topLeftY;
|
||||
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom(newTopLeftX, newTopLeftY),
|
||||
pointFrom(cx + changeInX, cy + changeInY),
|
||||
point(newTopLeftX, newTopLeftY),
|
||||
point(cx + changeInX, cy + changeInY),
|
||||
-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 { isEmbeddableElement } from "../../element/typeChecks";
|
||||
import { getLinkHandleFromCoords } from "./helpers";
|
||||
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||
import { point, type GlobalPoint } from "../../../math";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
@@ -181,7 +181,7 @@ export const Hyperlink = ({
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
pointFrom(event.clientX, event.clientY),
|
||||
point(event.clientX, event.clientY),
|
||||
) as boolean;
|
||||
if (shouldHide) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GlobalPoint, Radians } from "../../../math";
|
||||
import { pointFrom, pointRotateRads } from "../../../math";
|
||||
import { point, pointRotateRads } from "../../../math";
|
||||
import { MIME_TYPES } from "../../constants";
|
||||
import type { Bounds } from "../../element/bounds";
|
||||
import { getElementAbsoluteCoords } from "../../element/bounds";
|
||||
@@ -35,8 +35,8 @@ export const getLinkHandleFromCoords = (
|
||||
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
||||
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
pointFrom(x + linkWidth / 2, y + linkHeight / 2),
|
||||
pointFrom(centerX, centerY),
|
||||
point(x + linkWidth / 2, y + linkHeight / 2),
|
||||
point(centerX, centerY),
|
||||
angle,
|
||||
);
|
||||
return [
|
||||
@@ -85,10 +85,5 @@ export const isPointHittingLink = (
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return isPointHittingLinkIcon(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
pointFrom(x, y),
|
||||
);
|
||||
return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y));
|
||||
};
|
||||
|
||||
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"backgroundColor": "#d8f5a2",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id47",
|
||||
"id": "id45",
|
||||
"type": "arrow",
|
||||
},
|
||||
{
|
||||
"id": "id48",
|
||||
"id": "id46",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id48",
|
||||
"id": "id46",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id49",
|
||||
"elementId": "id47",
|
||||
"fixedPoint": null,
|
||||
"focus": -0.08139534883720931,
|
||||
"gap": 1,
|
||||
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id47",
|
||||
"id": "id45",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id50",
|
||||
"id": "id48",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id50",
|
||||
"id": "id48",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id51",
|
||||
"id": "id49",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"autoResize": true,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"containerId": "id50",
|
||||
"containerId": "id48",
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 5,
|
||||
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id40",
|
||||
"id": "id38",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"elbowed": false,
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id42",
|
||||
"elementId": "id40",
|
||||
"fixedPoint": null,
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id41",
|
||||
"elementId": "id39",
|
||||
"fixedPoint": null,
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"autoResize": true,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"containerId": "id39",
|
||||
"containerId": "id37",
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 5,
|
||||
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id39",
|
||||
"id": "id37",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id39",
|
||||
"id": "id37",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id44",
|
||||
"id": "id42",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"elbowed": false,
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id46",
|
||||
"elementId": "id44",
|
||||
"fixedPoint": null,
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id45",
|
||||
"elementId": "id43",
|
||||
"fixedPoint": null,
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"autoResize": true,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"containerId": "id43",
|
||||
"containerId": "id41",
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 5,
|
||||
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id43",
|
||||
"id": "id41",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id43",
|
||||
"id": "id41",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id56",
|
||||
"id": "id54",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id57",
|
||||
"id": "id55",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id58",
|
||||
"id": "id56",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id59",
|
||||
"id": "id57",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id60",
|
||||
"id": "id58",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id61",
|
||||
"id": "id59",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
getNormalizedZoom,
|
||||
} from "../scene";
|
||||
import type { LocalPoint, Radians } from "../../math";
|
||||
import { isFiniteNumber, pointFrom } from "../../math";
|
||||
import { isFiniteNumber, point } from "../../math";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
AppState,
|
||||
@@ -121,8 +121,7 @@ const repairBinding = (
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
/** @deprecated */
|
||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||
@@ -185,9 +184,6 @@ const restoreElementWithProperties = <
|
||||
locked: element.locked ?? false,
|
||||
};
|
||||
|
||||
if ("subtype" in element) {
|
||||
base.subtype = element.subtype;
|
||||
}
|
||||
if ("customData" in element || "customData" in extra) {
|
||||
base.customData =
|
||||
"customData" in extra ? extra.customData : element.customData;
|
||||
@@ -272,7 +268,7 @@ const restoreElement = (
|
||||
let y = element.y;
|
||||
let points = // migrate old arrow model to new one
|
||||
!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;
|
||||
|
||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||
@@ -300,7 +296,7 @@ const restoreElement = (
|
||||
let y: number | undefined = element.y;
|
||||
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||
!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;
|
||||
|
||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||
@@ -601,12 +597,6 @@ export const restoreAppState = (
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
if ("activeSubtypes" in appState) {
|
||||
nextAppState.activeSubtypes = appState.activeSubtypes;
|
||||
}
|
||||
if ("customData" in appState) {
|
||||
nextAppState.customData = appState.customData;
|
||||
}
|
||||
return {
|
||||
...nextAppState,
|
||||
cursorButton: localAppState?.cursorButton || "up",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { vi } from "vitest";
|
||||
import type { ExcalidrawElementSkeleton } from "./transform";
|
||||
import { convertToExcalidrawElements } from "./transform";
|
||||
import type { ExcalidrawArrowElement } from "../element/types";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
const opts = { regenerateIds: false };
|
||||
|
||||
@@ -309,32 +309,28 @@ describe("Test Transform", () => {
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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",
|
||||
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[] = [
|
||||
...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",
|
||||
children: ["1", "2"],
|
||||
@@ -373,27 +388,7 @@ describe("Test Transform", () => {
|
||||
);
|
||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||
expect(frame.width).toBe(800);
|
||||
expect(frame.height).toBe(100);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(frame.height).toBe(126);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -917,7 +912,7 @@ describe("Test Transform", () => {
|
||||
x: 111.262,
|
||||
y: 57,
|
||||
strokeWidth: 2,
|
||||
points: [pointFrom(0, 0), pointFrom(272.985, 0)],
|
||||
points: [point(0, 0), point(272.985, 0)],
|
||||
label: {
|
||||
text: "How are you?",
|
||||
fontSize: 20,
|
||||
@@ -940,7 +935,7 @@ describe("Test Transform", () => {
|
||||
x: 77.017,
|
||||
y: 79,
|
||||
strokeWidth: 2,
|
||||
points: [pointFrom(0, 0)],
|
||||
points: [point(0, 0)],
|
||||
label: {
|
||||
text: "Friendship",
|
||||
fontSize: 20,
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
assertNever,
|
||||
cloneJSON,
|
||||
getFontString,
|
||||
isDevEnv,
|
||||
toBrandedType,
|
||||
} from "../utils";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
@@ -54,7 +53,7 @@ import { randomId } from "../random";
|
||||
import { syncInvalidIndices } from "../fractionalIndex";
|
||||
import { getLineHeight } from "../fonts";
|
||||
import { isArrowElement } from "../element/typeChecks";
|
||||
import { pointFrom, type LocalPoint } from "../../math";
|
||||
import { point, type LocalPoint } from "../../math";
|
||||
|
||||
export type ValidLinearElement = {
|
||||
type: "arrow" | "line";
|
||||
@@ -537,7 +536,7 @@ export const convertToExcalidrawElements = (
|
||||
excalidrawElement = newLinearElement({
|
||||
width,
|
||||
height,
|
||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||
points: [point(0, 0), point(width, height)],
|
||||
...element,
|
||||
});
|
||||
|
||||
@@ -550,7 +549,7 @@ export const convertToExcalidrawElements = (
|
||||
width,
|
||||
height,
|
||||
endArrowhead: "arrow",
|
||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||
points: [point(0, 0), point(width, height)],
|
||||
...element,
|
||||
type: "arrow",
|
||||
});
|
||||
@@ -718,7 +717,7 @@ export const convertToExcalidrawElements = (
|
||||
}
|
||||
|
||||
// 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.
|
||||
for (const [id, element] of elementsWithIds) {
|
||||
if (element.type !== "frame" && element.type !== "magicframe") {
|
||||
@@ -765,26 +764,10 @@ export const convertToExcalidrawElements = (
|
||||
maxX = maxX + PADDING;
|
||||
maxY = maxY + PADDING;
|
||||
|
||||
const frameX = frame?.x || minX;
|
||||
const frameY = frame?.y || minY;
|
||||
const frameWidth = frame?.width || maxX - minX;
|
||||
const frameHeight = frame?.height || maxY - minY;
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
// Take the max of calculated and provided frame dimensions, whichever is higher
|
||||
const width = Math.max(frame?.width, maxX - minX);
|
||||
const height = Math.max(frame?.height, maxY - minY);
|
||||
Object.assign(frame, { x: minX, y: minY, width, height });
|
||||
}
|
||||
|
||||
return elementStore.getElements();
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
import type { LocalPoint, Radians } from "../../math";
|
||||
import {
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
point,
|
||||
pointRotateRads,
|
||||
type GlobalPoint,
|
||||
vectorFromPoint,
|
||||
@@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||
return vectorToHeading(
|
||||
vectorFromPoint(
|
||||
p,
|
||||
pointFrom<GlobalPoint>(
|
||||
point<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
),
|
||||
@@ -766,15 +766,15 @@ export const bindPointToSnapToElementOutline = (
|
||||
const intersections = [
|
||||
...(intersectElementWithLine(
|
||||
bindableElement,
|
||||
pointFrom(p[0], p[1] - 2 * bindableElement.height),
|
||||
pointFrom(p[0], p[1] + 2 * bindableElement.height),
|
||||
point(p[0], p[1] - 2 * bindableElement.height),
|
||||
point(p[0], p[1] + 2 * bindableElement.height),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
) ?? []),
|
||||
...(intersectElementWithLine(
|
||||
bindableElement,
|
||||
pointFrom(p[0] - 2 * bindableElement.width, p[1]),
|
||||
pointFrom(p[0] + 2 * bindableElement.width, p[1]),
|
||||
point(p[0] - 2 * bindableElement.width, p[1]),
|
||||
point(p[0] + 2 * bindableElement.width, p[1]),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
) ?? []),
|
||||
@@ -815,25 +815,25 @@ const headingToMidBindPoint = (
|
||||
switch (true) {
|
||||
case compareHeading(heading, HEADING_UP):
|
||||
return pointRotateRads(
|
||||
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||
point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
case compareHeading(heading, HEADING_RIGHT):
|
||||
return pointRotateRads(
|
||||
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||
point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
case compareHeading(heading, HEADING_DOWN):
|
||||
return pointRotateRads(
|
||||
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
default:
|
||||
return pointRotateRads(
|
||||
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||
point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
@@ -844,7 +844,7 @@ export const avoidRectangularCorner = (
|
||||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
const center = point<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
@@ -854,13 +854,13 @@ export const avoidRectangularCorner = (
|
||||
// Top left
|
||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||
point(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||
point(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -871,16 +871,13 @@ export const avoidRectangularCorner = (
|
||||
// Bottom left
|
||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x,
|
||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -894,7 +891,7 @@ export const avoidRectangularCorner = (
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.x + element.width,
|
||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
@@ -903,7 +900,7 @@ export const avoidRectangularCorner = (
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.x + element.width + FIXED_BINDING_DISTANCE,
|
||||
element.y + element.height,
|
||||
),
|
||||
@@ -920,16 +917,13 @@ export const avoidRectangularCorner = (
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x + element.width,
|
||||
element.y - FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -944,10 +938,7 @@ export const snapToMid = (
|
||||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
x + width / 2 - 0.1,
|
||||
y + height / 2 - 0.1,
|
||||
);
|
||||
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
@@ -962,7 +953,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads(
|
||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||
point(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -973,7 +964,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y - FIXED_BINDING_DISTANCE),
|
||||
point(center[0], y - FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -984,7 +975,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||
point(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -995,7 +986,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||
point(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1032,11 +1023,11 @@ const updateBoundPoint = (
|
||||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
const globalMidPoint = point<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
const global = point<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
);
|
||||
@@ -1127,7 +1118,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
);
|
||||
const globalMidPoint = pointFrom(
|
||||
const globalMidPoint = point(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||
);
|
||||
@@ -1346,9 +1337,9 @@ export const bindingBorderTest = (
|
||||
const threshold = maxBindingGap(element, element.width, element.height);
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
return (
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||
isPointOnShape(point(x, y), shape, threshold) ||
|
||||
(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);
|
||||
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.x + element.width * fixedX,
|
||||
element.y + element.height * fixedY,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
point<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
@@ -2238,7 +2229,7 @@ const getGlobalFixedPoints = (
|
||||
arrow.startBinding.fixedPoint,
|
||||
startElement as ExcalidrawBindableElement,
|
||||
)
|
||||
: pointFrom<GlobalPoint>(
|
||||
: point<GlobalPoint>(
|
||||
arrow.x + arrow.points[0][0],
|
||||
arrow.y + arrow.points[0][1],
|
||||
);
|
||||
@@ -2248,7 +2239,7 @@ const getGlobalFixedPoints = (
|
||||
arrow.endBinding.fixedPoint,
|
||||
endElement as ExcalidrawBindableElement,
|
||||
)
|
||||
: pointFrom<GlobalPoint>(
|
||||
: point<GlobalPoint>(
|
||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||
arrow.y + arrow.points[arrow.points.length - 1][1],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
import { ROUNDNESS } from "../constants";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||
@@ -125,9 +125,9 @@ describe("getElementBounds", () => {
|
||||
a: 0.6447741904932416,
|
||||
}),
|
||||
points: [
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(67.33984375, 92.48828125),
|
||||
pointFrom<LocalPoint>(-102.7890625, 52.15625),
|
||||
point<LocalPoint>(0, 0),
|
||||
point<LocalPoint>(67.33984375, 92.48828125),
|
||||
point<LocalPoint>(-102.7890625, 52.15625),
|
||||
],
|
||||
} as ExcalidrawLinearElement;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
point,
|
||||
pointDistance,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
@@ -113,8 +113,8 @@ export class ElementBounds {
|
||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||
element.points.map(([x, y]) =>
|
||||
pointRotateRads(
|
||||
pointFrom(x, y),
|
||||
pointFrom(cx - element.x, cy - element.y),
|
||||
point(x, y),
|
||||
point(cx - element.x, cy - element.y),
|
||||
element.angle,
|
||||
),
|
||||
),
|
||||
@@ -130,23 +130,23 @@ export class ElementBounds {
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||
} else if (element.type === "diamond") {
|
||||
const [x11, y11] = pointRotateRads(
|
||||
pointFrom(cx, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(cx, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x12, y12] = pointRotateRads(
|
||||
pointFrom(cx, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(cx, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x22, y22] = pointRotateRads(
|
||||
pointFrom(x1, cy),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, cy),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x21, y21] = pointRotateRads(
|
||||
pointFrom(x2, cy),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, cy),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const minX = Math.min(x11, x12, x22, x21);
|
||||
@@ -164,23 +164,23 @@ export class ElementBounds {
|
||||
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||
} else {
|
||||
const [x11, y11] = pointRotateRads(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x12, y12] = pointRotateRads(
|
||||
pointFrom(x1, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x22, y22] = pointRotateRads(
|
||||
pointFrom(x2, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x21, y21] = pointRotateRads(
|
||||
pointFrom(x2, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const minX = Math.min(x11, x12, x22, x21);
|
||||
@@ -255,7 +255,7 @@ export const getElementLineSegments = (
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center: GlobalPoint = pointFrom(cx, cy);
|
||||
const center: GlobalPoint = point(cx, cy);
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
@@ -266,7 +266,7 @@ export const getElementLineSegments = (
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.points[i][0] + element.x,
|
||||
element.points[i][1] + element.y,
|
||||
),
|
||||
@@ -274,7 +274,7 @@ export const getElementLineSegments = (
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.points[i + 1][0] + element.x,
|
||||
element.points[i + 1][1] + element.y,
|
||||
),
|
||||
@@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
ops: Op[],
|
||||
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
||||
): Bounds => {
|
||||
let currentP: GlobalPoint = pointFrom(0, 0);
|
||||
let currentP: GlobalPoint = point(0, 0);
|
||||
|
||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||
(limits, { op, data }) => {
|
||||
@@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
// move operation does not draw anything; so, it always
|
||||
// returns false
|
||||
} else if (op === "bcurveTo") {
|
||||
const _p1 = pointFrom<GlobalPoint>(data[0], data[1]);
|
||||
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
|
||||
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
|
||||
const _p1 = point<GlobalPoint>(data[0], data[1]);
|
||||
const _p2 = point<GlobalPoint>(data[2], data[3]);
|
||||
const _p3 = point<GlobalPoint>(data[4], data[5]);
|
||||
|
||||
const p1 = transformXY ? transformXY(_p1) : _p1;
|
||||
const p2 = transformXY ? transformXY(_p2) : _p2;
|
||||
@@ -591,21 +591,21 @@ export const getArrowheadPoints = (
|
||||
|
||||
invariant(data.length === 6, "Op data length is not 6");
|
||||
|
||||
const p3 = pointFrom(data[4], data[5]);
|
||||
const p2 = pointFrom(data[2], data[3]);
|
||||
const p1 = pointFrom(data[0], data[1]);
|
||||
const p3 = point(data[4], data[5]);
|
||||
const p2 = point(data[2], data[3]);
|
||||
const p1 = point(data[0], data[1]);
|
||||
|
||||
// We need to find p0 of the bezier curve.
|
||||
// It is typically the last point of the previous
|
||||
// curve; it can also be the position of moveTo operation.
|
||||
const prevOp = ops[index - 1];
|
||||
let p0 = pointFrom(0, 0);
|
||||
let p0 = point(0, 0);
|
||||
if (prevOp.op === "move") {
|
||||
const p = pointFromArray(prevOp.data);
|
||||
invariant(p != null, "Op data is not a point");
|
||||
p0 = p;
|
||||
} 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
|
||||
@@ -671,13 +671,13 @@ export const getArrowheadPoints = (
|
||||
|
||||
// Return points
|
||||
const [x3, y3] = pointRotateRads(
|
||||
pointFrom(xs, ys),
|
||||
pointFrom(x2, y2),
|
||||
point(xs, ys),
|
||||
point(x2, y2),
|
||||
((-angle * Math.PI) / 180) as Radians,
|
||||
);
|
||||
const [x4, y4] = pointRotateRads(
|
||||
pointFrom(xs, ys),
|
||||
pointFrom(x2, y2),
|
||||
point(xs, ys),
|
||||
point(x2, y2),
|
||||
degreesToRadians(angle),
|
||||
);
|
||||
|
||||
@@ -690,8 +690,8 @@ export const getArrowheadPoints = (
|
||||
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||
|
||||
[ox, oy] = pointRotateRads(
|
||||
pointFrom(x2 + minSize * 2, y2),
|
||||
pointFrom(x2, y2),
|
||||
point(x2 + minSize * 2, y2),
|
||||
point(x2, y2),
|
||||
Math.atan2(py - y2, px - x2) as Radians,
|
||||
);
|
||||
} else {
|
||||
@@ -701,8 +701,8 @@ export const getArrowheadPoints = (
|
||||
: [0, 0];
|
||||
|
||||
[ox, oy] = pointRotateRads(
|
||||
pointFrom(x2 - minSize * 2, y2),
|
||||
pointFrom(x2, y2),
|
||||
point(x2 - minSize * 2, y2),
|
||||
point(x2, y2),
|
||||
Math.atan2(y2 - py, x2 - px) as Radians,
|
||||
);
|
||||
}
|
||||
@@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = (
|
||||
if (element.points.length < 2) {
|
||||
const [pointX, pointY] = element.points[0];
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom(element.x + pointX, element.y + pointY),
|
||||
pointFrom(cx, cy),
|
||||
point(element.x + pointX, element.y + pointY),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
@@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = (
|
||||
const ops = getCurvePathOps(shape);
|
||||
const transformXY = ([x, y]: GlobalPoint) =>
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(element.x + x, element.y + y),
|
||||
pointFrom(cx, cy),
|
||||
point(element.x + x, element.y + y),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
@@ -931,8 +931,8 @@ export const getClosestElementBounds = (
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||
const distance = pointDistance(
|
||||
pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
pointFrom(from.x, from.y),
|
||||
point((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
point(from.x, from.y),
|
||||
);
|
||||
|
||||
if (distance < minDistance) {
|
||||
@@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({
|
||||
};
|
||||
|
||||
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
||||
pointFrom(
|
||||
point(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "./typeChecks";
|
||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
||||
import { isPointWithinBounds, pointFrom } from "../../math";
|
||||
import { isPointWithinBounds, point } from "../../math";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
if (element.type === "arrow") {
|
||||
@@ -61,13 +61,13 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||
let hit = shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInShape(pointFrom(x, y), shape) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
||||
isPointInShape(point(x, y), shape) ||
|
||||
isPointOnShape(point(x, y), shape, threshold)
|
||||
: isPointOnShape(point(x, y), shape, threshold);
|
||||
|
||||
// hit test against a frame's name
|
||||
if (!hit && frameNameBound) {
|
||||
hit = isPointInShape(pointFrom(x, y), {
|
||||
hit = isPointInShape(point(x, y), {
|
||||
type: "polygon",
|
||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||
.data as Polygon<Point>,
|
||||
@@ -89,11 +89,7 @@ export const hitElementBoundingBox = (
|
||||
y1 -= tolerance;
|
||||
x2 += tolerance;
|
||||
y2 += tolerance;
|
||||
return isPointWithinBounds(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(x, y),
|
||||
pointFrom(x2, y2),
|
||||
);
|
||||
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
||||
};
|
||||
|
||||
export const hitElementBoundingBoxOnly = <
|
||||
@@ -119,5 +115,5 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
||||
y: number,
|
||||
textShape: GeometricShape<Point> | null,
|
||||
): 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 =
|
||||
/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([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
@@ -65,7 +59,6 @@ const ALLOWED_DOMAINS = new Set([
|
||||
"stackblitz.com",
|
||||
"val.town",
|
||||
"giphy.com",
|
||||
"reddit.com",
|
||||
]);
|
||||
|
||||
const ALLOW_SAME_ORIGIN = new Set([
|
||||
@@ -78,7 +71,6 @@ const ALLOW_SAME_ORIGIN = new Set([
|
||||
"x.com",
|
||||
"*.simplepdf.eu",
|
||||
"stackblitz.com",
|
||||
"reddit.com",
|
||||
]);
|
||||
|
||||
export const createSrcDoc = (body: string) => {
|
||||
@@ -226,24 +218,6 @@ export const getEmbedLink = (
|
||||
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)) {
|
||||
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
||||
const safeURL = sanitizeHTMLAttribute(
|
||||
@@ -387,11 +361,6 @@ export const maybeParseEmbedSrc = (str: string): string => {
|
||||
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);
|
||||
if (gistMatch && gistMatch.length === 2) {
|
||||
return gistMatch[1];
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
isFlowchartNodeElement,
|
||||
} from "./typeChecks";
|
||||
import { invariant } from "../utils";
|
||||
import { pointFrom, type LocalPoint } from "../../math";
|
||||
import { point, type LocalPoint } from "../../math";
|
||||
import { aabbForElement } from "../shapes";
|
||||
|
||||
type LinkDirection = "up" | "right" | "down" | "left";
|
||||
@@ -421,7 +421,7 @@ const createBindingArrow = (
|
||||
strokeColor: appState.currentItemStrokeColor,
|
||||
strokeStyle: appState.currentItemStrokeStyle,
|
||||
strokeWidth: appState.currentItemStrokeWidth,
|
||||
points: [pointFrom(0, 0), pointFrom(endX, endY)],
|
||||
points: [point(0, 0), point(endX, endY)],
|
||||
elbowed: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
Radians,
|
||||
} from "../../math";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
pointRotateRads,
|
||||
pointScaleFromOrigin,
|
||||
radiansToDegrees,
|
||||
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
|
||||
|
||||
const top = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width / 2, element.y),
|
||||
point(element.x + element.width / 2, element.y),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -91,7 +91,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const right = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width, element.y + element.height / 2),
|
||||
point(element.x + element.width, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -100,7 +100,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const bottom = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height),
|
||||
point(element.x + element.width / 2, element.y + element.height),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -109,7 +109,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const left = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x, element.y + element.height / 2),
|
||||
point(element.x, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -133,22 +133,22 @@ export const headingForPointFromElement = <
|
||||
}
|
||||
|
||||
const topLeft = pointScaleFromOrigin(
|
||||
pointFrom(aabb[0], aabb[1]),
|
||||
point(aabb[0], aabb[1]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const topRight = pointScaleFromOrigin(
|
||||
pointFrom(aabb[2], aabb[1]),
|
||||
point(aabb[2], aabb[1]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const bottomLeft = pointScaleFromOrigin(
|
||||
pointFrom(aabb[0], aabb[3]),
|
||||
point(aabb[0], aabb[3]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const bottomRight = pointScaleFromOrigin(
|
||||
pointFrom(aabb[2], aabb[3]),
|
||||
point(aabb[2], aabb[3]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
|
||||
@@ -49,7 +49,7 @@ import type Scene from "../scene/Scene";
|
||||
import type { Radians } from "../../math";
|
||||
import {
|
||||
pointCenter,
|
||||
pointFrom,
|
||||
point,
|
||||
pointRotateRads,
|
||||
pointsEqual,
|
||||
vector,
|
||||
@@ -108,7 +108,7 @@ export class LinearElementEditor {
|
||||
this.elementId = element.id as string & {
|
||||
_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);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
elementsMap,
|
||||
referencePoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
point(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
@@ -296,7 +296,7 @@ export class LinearElementEditor {
|
||||
[
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: pointFrom(
|
||||
point: point(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
),
|
||||
@@ -329,7 +329,7 @@ export class LinearElementEditor {
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
)
|
||||
: pointFrom(
|
||||
: point(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
@@ -590,11 +590,11 @@ export class LinearElementEditor {
|
||||
linearElementEditor.segmentMidPointHoveredCoords;
|
||||
if (existingSegmentMidpointHitCoords) {
|
||||
const distance = pointDistance(
|
||||
pointFrom(
|
||||
point(
|
||||
existingSegmentMidpointHitCoords[0],
|
||||
existingSegmentMidpointHitCoords[1],
|
||||
),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return existingSegmentMidpointHitCoords;
|
||||
@@ -606,8 +606,8 @@ export class LinearElementEditor {
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = pointDistance(
|
||||
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
point(midPoints[index]![0], midPoints[index]![1]),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return midPoints[index];
|
||||
@@ -626,8 +626,8 @@ export class LinearElementEditor {
|
||||
zoom: AppState["zoom"],
|
||||
) {
|
||||
let distance = pointDistance(
|
||||
pointFrom(startPoint[0], startPoint[1]),
|
||||
pointFrom(endPoint[0], endPoint[1]),
|
||||
point(startPoint[0], startPoint[1]),
|
||||
point(endPoint[0], endPoint[1]),
|
||||
);
|
||||
if (element.points.length > 2 && element.roundness) {
|
||||
distance = getBezierCurveLength(element, endPoint);
|
||||
@@ -829,11 +829,11 @@ export class LinearElementEditor {
|
||||
const targetPoint =
|
||||
clickedPointIndex > -1 &&
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
point(
|
||||
element.x + element.points[clickedPointIndex][0],
|
||||
element.y + element.points[clickedPointIndex][1],
|
||||
),
|
||||
pointFrom(cx, cy),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
@@ -928,11 +928,11 @@ export class LinearElementEditor {
|
||||
element,
|
||||
elementsMap,
|
||||
lastCommittedPoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
point(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
newPoint = pointFrom(
|
||||
newPoint = point(
|
||||
width + lastCommittedPoint[0],
|
||||
height + lastCommittedPoint[1],
|
||||
);
|
||||
@@ -984,8 +984,8 @@ export class LinearElementEditor {
|
||||
|
||||
const { x, y } = element;
|
||||
return pointRotateRads(
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
point(x + p[0], y + p[1]),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
@@ -1001,8 +1001,8 @@ export class LinearElementEditor {
|
||||
return element.points.map((p) => {
|
||||
const { x, y } = element;
|
||||
return pointRotateRads(
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
point(x + p[0], y + p[1]),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
});
|
||||
@@ -1025,12 +1025,8 @@ export class LinearElementEditor {
|
||||
const { x, y } = element;
|
||||
|
||||
return p
|
||||
? pointRotateRads(
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
)
|
||||
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
|
||||
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
|
||||
: pointRotateRads(point(x, y), point(cx, cy), element.angle);
|
||||
}
|
||||
|
||||
static pointFromAbsoluteCoords(
|
||||
@@ -1040,7 +1036,7 @@ export class LinearElementEditor {
|
||||
): LocalPoint {
|
||||
if (isElbowArrow(element)) {
|
||||
// No rotation for elbow arrows
|
||||
return pointFrom(
|
||||
return point(
|
||||
absoluteCoords[0] - element.x,
|
||||
absoluteCoords[1] - element.y,
|
||||
);
|
||||
@@ -1050,11 +1046,11 @@ export class LinearElementEditor {
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom(absoluteCoords[0], absoluteCoords[1]),
|
||||
pointFrom(cx, cy),
|
||||
point(absoluteCoords[0], absoluteCoords[1]),
|
||||
point(cx, cy),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
return pointFrom(x - element.x, y - element.y);
|
||||
return point(x - element.x, y - element.y);
|
||||
}
|
||||
|
||||
static getPointIndexUnderCursor(
|
||||
@@ -1075,7 +1071,7 @@ export class LinearElementEditor {
|
||||
while (--idx > -1) {
|
||||
const p = pointHandles[idx];
|
||||
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
|
||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||
) {
|
||||
@@ -1097,12 +1093,12 @@ export class LinearElementEditor {
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
pointFrom(pointerOnGrid[0], pointerOnGrid[1]),
|
||||
pointFrom(cx, cy),
|
||||
point(pointerOnGrid[0], pointerOnGrid[1]),
|
||||
point(cx, cy),
|
||||
-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 {
|
||||
points: points.map((p) => {
|
||||
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
||||
return point(p[0] - offsetX, p[1] - offsetY);
|
||||
}),
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
@@ -1176,8 +1172,8 @@ export class LinearElementEditor {
|
||||
}
|
||||
acc.push(
|
||||
nextPoint
|
||||
? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||
: pointFrom(p[0], p[1]),
|
||||
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||
: point(p[0], p[1]),
|
||||
);
|
||||
|
||||
nextSelectedIndices.push(indexCursor + 1);
|
||||
@@ -1198,7 +1194,7 @@ export class LinearElementEditor {
|
||||
[
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
point: point(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
@@ -1239,9 +1235,7 @@ export class LinearElementEditor {
|
||||
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
||||
if (!pointIndices.includes(idx)) {
|
||||
acc.push(
|
||||
!acc.length
|
||||
? pointFrom(0, 0)
|
||||
: pointFrom(p[0] - offsetX, p[1] - offsetY),
|
||||
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
@@ -1318,9 +1312,9 @@ export class LinearElementEditor {
|
||||
const deltaY =
|
||||
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(
|
||||
@@ -1374,8 +1368,8 @@ export class LinearElementEditor {
|
||||
|
||||
const origin = linearElementEditor.pointerDownState.origin!;
|
||||
const dist = pointDistance(
|
||||
pointFrom(origin.x, origin.y),
|
||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||
point(origin.x, origin.y),
|
||||
point(pointerCoords.x, pointerCoords.y),
|
||||
);
|
||||
if (
|
||||
!appState.editingLinearElement &&
|
||||
@@ -1499,8 +1493,8 @@ export class LinearElementEditor {
|
||||
const dX = prevCenterX - nextCenterX;
|
||||
const dY = prevCenterY - nextCenterY;
|
||||
const rotated = pointRotateRads(
|
||||
pointFrom(offsetX, offsetY),
|
||||
pointFrom(dX, dY),
|
||||
point(offsetX, offsetY),
|
||||
point(dX, dY),
|
||||
element.angle,
|
||||
);
|
||||
mutateElement(element, {
|
||||
@@ -1546,8 +1540,8 @@ export class LinearElementEditor {
|
||||
);
|
||||
|
||||
return pointRotateRads(
|
||||
pointFrom(width, height),
|
||||
pointFrom(0, 0),
|
||||
point(width, height),
|
||||
point(0, 0),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
}
|
||||
@@ -1617,36 +1611,36 @@ export class LinearElementEditor {
|
||||
);
|
||||
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
||||
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
||||
const centerPoint = pointFrom(cx, cy);
|
||||
const centerPoint = point(cx, cy);
|
||||
|
||||
const topLeftRotatedPoint = pointRotateRads(
|
||||
pointFrom(x1, y1),
|
||||
point(x1, y1),
|
||||
centerPoint,
|
||||
element.angle,
|
||||
);
|
||||
const topRightRotatedPoint = pointRotateRads(
|
||||
pointFrom(x2, y1),
|
||||
point(x2, y1),
|
||||
centerPoint,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
const counterRotateBoundTextTopLeft = pointRotateRads(
|
||||
pointFrom(boundTextX1, boundTextY1),
|
||||
point(boundTextX1, boundTextY1),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextTopRight = pointRotateRads(
|
||||
pointFrom(boundTextX2, boundTextY1),
|
||||
point(boundTextX2, boundTextY1),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
||||
pointFrom(boundTextX1, boundTextY2),
|
||||
point(boundTextX1, boundTextY2),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextBottomRight = pointRotateRads(
|
||||
pointFrom(boundTextX2, boundTextY2),
|
||||
point(boundTextX2, boundTextY2),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
@@ -5,23 +5,12 @@ import { randomInteger } from "../random";
|
||||
import { getUpdatedTimestamp } from "../utils";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { maybeGetSubtypeProps } from "./newElement";
|
||||
import { getSubtypeMethods } from "./subtypes";
|
||||
|
||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"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.
|
||||
// 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
|
||||
@@ -32,8 +21,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
informMutation = true,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
@@ -82,7 +69,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
}
|
||||
}
|
||||
if (!didChangePoints) {
|
||||
key in oldUpdates && (increment = true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +76,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
(element as any)[key] = value;
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +92,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
|
||||
if (increment) {
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
}
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.triggerUpdate();
|
||||
@@ -127,8 +110,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
force = false,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
for (const key in updates) {
|
||||
const value = (updates as any)[key];
|
||||
if (typeof value !== "undefined") {
|
||||
@@ -140,7 +121,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
continue;
|
||||
}
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,9 +128,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!increment) {
|
||||
return { ...element, ...updates };
|
||||
}
|
||||
return {
|
||||
...element,
|
||||
...updates,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants";
|
||||
import { isPrimitive } from "../utils";
|
||||
import type { ExcalidrawLinearElement } from "./types";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
const assertCloneObjects = (source: any, clone: any) => {
|
||||
for (const key in clone) {
|
||||
@@ -38,7 +38,7 @@ describe("duplicating single elements", () => {
|
||||
element.__proto__ = { hello: "world" };
|
||||
|
||||
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);
|
||||
|
||||
@@ -19,7 +19,12 @@ import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
} from "./types";
|
||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import {
|
||||
arrayToMap,
|
||||
getFontString,
|
||||
getUpdatedTimestamp,
|
||||
isTestEnv,
|
||||
} from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
import { bumpVersion, newElementWith } from "./mutateElement";
|
||||
import { getNewGroupIdsForDuplication } from "../groups";
|
||||
@@ -27,9 +32,9 @@ import type { AppState } from "../types";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
measureTextElement,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapTextElement,
|
||||
wrapText,
|
||||
getBoundTextMaxWidth,
|
||||
} from "./textElement";
|
||||
import {
|
||||
@@ -43,30 +48,6 @@ import {
|
||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||
import { getLineHeight } from "../fonts";
|
||||
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<
|
||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||
@@ -81,8 +62,6 @@ export type ElementConstructorOpts = MarkOptional<
|
||||
| "version"
|
||||
| "versionNonce"
|
||||
| "link"
|
||||
| "subtype"
|
||||
| "customData"
|
||||
| "strokeStyle"
|
||||
| "fillStyle"
|
||||
| "strokeColor"
|
||||
@@ -120,10 +99,8 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
||||
...rest
|
||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||
) => {
|
||||
const { subtype, customData } = rest;
|
||||
// assign type to guard against excess properties
|
||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||
...maybeGetSubtypeProps({ subtype, customData }, type),
|
||||
id: rest.id || randomId(),
|
||||
type,
|
||||
x,
|
||||
@@ -159,11 +136,8 @@ export const newElement = (
|
||||
opts: {
|
||||
type: ExcalidrawGenericElement["type"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawGenericElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
};
|
||||
): NonDeleted<ExcalidrawGenericElement> =>
|
||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
|
||||
export const newEmbeddableElement = (
|
||||
opts: {
|
||||
@@ -256,12 +230,10 @@ export const newTextElement = (
|
||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
||||
const text = normalizeText(opts.text);
|
||||
const metrics = measureTextElement(
|
||||
{ ...opts, fontSize, fontFamily, lineHeight },
|
||||
{
|
||||
text,
|
||||
customData: opts.customData,
|
||||
},
|
||||
const metrics = measureText(
|
||||
text,
|
||||
getFontString({ fontFamily, fontSize }),
|
||||
lineHeight,
|
||||
);
|
||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||
@@ -305,9 +277,11 @@ const getAdjustedDimensions = (
|
||||
width: number;
|
||||
height: number;
|
||||
} => {
|
||||
let { width: nextWidth, height: nextHeight } = measureTextElement(element, {
|
||||
text: nextText,
|
||||
});
|
||||
let { width: nextWidth, height: nextHeight } = measureText(
|
||||
nextText,
|
||||
getFontString(element),
|
||||
element.lineHeight,
|
||||
);
|
||||
|
||||
// wrapped text
|
||||
if (!element.autoResize) {
|
||||
@@ -323,7 +297,11 @@ const getAdjustedDimensions = (
|
||||
!element.containerId &&
|
||||
element.autoResize
|
||||
) {
|
||||
const prevMetrics = measureTextElement(element);
|
||||
const prevMetrics = measureText(
|
||||
element.text,
|
||||
getFontString(element),
|
||||
element.lineHeight,
|
||||
);
|
||||
const offsets = getTextElementPositionOffsets(element, {
|
||||
width: nextWidth - prevMetrics.width,
|
||||
height: nextHeight - prevMetrics.height,
|
||||
@@ -426,14 +404,12 @@ export const refreshTextDimensions = (
|
||||
return;
|
||||
}
|
||||
if (container || !textElement.autoResize) {
|
||||
text = wrapTextElement(
|
||||
textElement,
|
||||
text = wrapText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
container
|
||||
? getBoundTextMaxWidth(container, textElement)
|
||||
: textElement.width,
|
||||
{
|
||||
text,
|
||||
},
|
||||
);
|
||||
}
|
||||
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
|
||||
@@ -448,8 +424,6 @@ export const newFreeDrawElement = (
|
||||
pressures?: ExcalidrawFreeDrawElement["pressures"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
@@ -465,8 +439,6 @@ export const newLinearElement = (
|
||||
points?: ExcalidrawLinearElement["points"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawLinearElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
@@ -487,8 +459,6 @@ export const newArrowElement = (
|
||||
elbowed?: boolean;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawArrowElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
@@ -509,8 +479,6 @@ export const newImageElement = (
|
||||
scale?: ExcalidrawImageElement["scale"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawImageElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
||||
// in the future we'll support changing stroke color for some SVG elements,
|
||||
|
||||
@@ -58,7 +58,7 @@ import type { GlobalPoint } from "../../math";
|
||||
import {
|
||||
pointCenter,
|
||||
normalizeRadians,
|
||||
pointFrom,
|
||||
point,
|
||||
pointFromPair,
|
||||
pointRotateRads,
|
||||
type Radians,
|
||||
@@ -240,8 +240,8 @@ const resizeSingleTextElement = (
|
||||
);
|
||||
// rotation pointer with reverse angle
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(cx, cy),
|
||||
point(pointerX, pointerY),
|
||||
point(cx, cy),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
let scaleX = 0;
|
||||
@@ -276,23 +276,23 @@ const resizeSingleTextElement = (
|
||||
const startBottomRight = [x2, y2];
|
||||
const startCenter = [cx, cy];
|
||||
|
||||
let newTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||
let newTopLeft = point<GlobalPoint>(x1, y1);
|
||||
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
startBottomRight[0] - Math.abs(nextWidth),
|
||||
startBottomRight[1] - Math.abs(nextHeight),
|
||||
);
|
||||
}
|
||||
if (transformHandleType === "ne") {
|
||||
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
bottomLeft[0],
|
||||
bottomLeft[1] - Math.abs(nextHeight),
|
||||
);
|
||||
}
|
||||
if (transformHandleType === "sw") {
|
||||
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
topRight[0] - Math.abs(nextWidth),
|
||||
topRight[1],
|
||||
);
|
||||
@@ -311,20 +311,12 @@ const resizeSingleTextElement = (
|
||||
}
|
||||
|
||||
const angle = element.angle;
|
||||
const rotatedTopLeft = pointRotateRads(
|
||||
newTopLeft,
|
||||
pointFrom(cx, cy),
|
||||
angle,
|
||||
);
|
||||
const newCenter = pointFrom<GlobalPoint>(
|
||||
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
|
||||
const newCenter = point<GlobalPoint>(
|
||||
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
||||
);
|
||||
const rotatedNewCenter = pointRotateRads(
|
||||
newCenter,
|
||||
pointFrom(cx, cy),
|
||||
angle,
|
||||
);
|
||||
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
|
||||
newTopLeft = pointRotateRads(
|
||||
rotatedTopLeft,
|
||||
rotatedNewCenter,
|
||||
@@ -349,12 +341,12 @@ const resizeSingleTextElement = (
|
||||
stateAtResizeStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||
const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
|
||||
const startTopLeft = point<GlobalPoint>(x1, y1);
|
||||
const startBottomRight = point<GlobalPoint>(x2, y2);
|
||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
point(pointerX, pointerY),
|
||||
startCenter,
|
||||
-stateAtResizeStart.angle as Radians,
|
||||
);
|
||||
@@ -427,7 +419,7 @@ const resizeSingleTextElement = (
|
||||
startCenter,
|
||||
angle,
|
||||
);
|
||||
const newCenter = pointFrom(
|
||||
const newCenter = point(
|
||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||
);
|
||||
@@ -469,13 +461,13 @@ export const resizeSingleElement = (
|
||||
stateAtResizeStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft = pointFrom(x1, y1);
|
||||
const startBottomRight = pointFrom(x2, y2);
|
||||
const startTopLeft = point(x1, y1);
|
||||
const startBottomRight = point(x2, y2);
|
||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||
|
||||
// Calculate new dimensions based on cursor position
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
point(pointerX, pointerY),
|
||||
startCenter,
|
||||
-stateAtResizeStart.angle as Radians,
|
||||
);
|
||||
@@ -656,7 +648,7 @@ export const resizeSingleElement = (
|
||||
startCenter,
|
||||
angle,
|
||||
);
|
||||
const newCenter = pointFrom(
|
||||
const newCenter = point(
|
||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||
);
|
||||
@@ -825,20 +817,20 @@ export const resizeMultipleElements = (
|
||||
const direction = transformHandleType;
|
||||
|
||||
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
||||
ne: pointFrom(minX, maxY),
|
||||
se: pointFrom(minX, minY),
|
||||
sw: pointFrom(maxX, minY),
|
||||
nw: pointFrom(maxX, maxY),
|
||||
e: pointFrom(minX, minY + height / 2),
|
||||
w: pointFrom(maxX, minY + height / 2),
|
||||
n: pointFrom(minX + width / 2, maxY),
|
||||
s: pointFrom(minX + width / 2, minY),
|
||||
ne: point(minX, maxY),
|
||||
se: point(minX, minY),
|
||||
sw: point(maxX, minY),
|
||||
nw: point(maxX, maxY),
|
||||
e: point(minX, minY + height / 2),
|
||||
w: point(maxX, minY + height / 2),
|
||||
n: point(minX + width / 2, maxY),
|
||||
s: point(minX + width / 2, minY),
|
||||
};
|
||||
|
||||
// anchor point must be on the opposite side of the dragged selection handle
|
||||
// or be the center of the selection if shouldResizeFromCenter
|
||||
const [anchorX, anchorY] = shouldResizeFromCenter
|
||||
? pointFrom(midX, midY)
|
||||
? point(midX, midY)
|
||||
: anchorsMap[direction];
|
||||
|
||||
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||
@@ -1052,8 +1044,8 @@ const rotateMultipleElements = (
|
||||
const origAngle =
|
||||
originalElements.get(element.id)?.angle ?? element.angle;
|
||||
const [rotatedCX, rotatedCY] = pointRotateRads(
|
||||
pointFrom(cx, cy),
|
||||
pointFrom(centerX, centerY),
|
||||
point(cx, cy),
|
||||
point(centerX, centerY),
|
||||
(centerAngle + origAngle - element.angle) as Radians,
|
||||
);
|
||||
|
||||
@@ -1109,44 +1101,40 @@ export const getResizeOffsetXY = (
|
||||
const angle = (
|
||||
selectedElements.length === 1 ? selectedElements[0].angle : 0
|
||||
) as Radians;
|
||||
[x, y] = pointRotateRads(
|
||||
pointFrom(x, y),
|
||||
pointFrom(cx, cy),
|
||||
-angle as Radians,
|
||||
);
|
||||
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
|
||||
switch (transformHandleType) {
|
||||
case "n":
|
||||
return pointRotateRads(
|
||||
pointFrom(x - (x1 + x2) / 2, y - y1),
|
||||
pointFrom(0, 0),
|
||||
point(x - (x1 + x2) / 2, y - y1),
|
||||
point(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "s":
|
||||
return pointRotateRads(
|
||||
pointFrom(x - (x1 + x2) / 2, y - y2),
|
||||
pointFrom(0, 0),
|
||||
point(x - (x1 + x2) / 2, y - y2),
|
||||
point(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "w":
|
||||
return pointRotateRads(
|
||||
pointFrom(x - x1, y - (y1 + y2) / 2),
|
||||
pointFrom(0, 0),
|
||||
point(x - x1, y - (y1 + y2) / 2),
|
||||
point(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "e":
|
||||
return pointRotateRads(
|
||||
pointFrom(x - x2, y - (y1 + y2) / 2),
|
||||
pointFrom(0, 0),
|
||||
point(x - x2, y - (y1 + y2) / 2),
|
||||
point(0, 0),
|
||||
angle,
|
||||
);
|
||||
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":
|
||||
return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
|
||||
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
|
||||
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":
|
||||
return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
|
||||
return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle);
|
||||
default:
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
pointOnLineSegment,
|
||||
pointRotateRads,
|
||||
type Radians,
|
||||
@@ -92,20 +92,16 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
||||
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
const sides = getSelectionBorders(
|
||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||
pointFrom(cx, cy),
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
for (const [dir, side] of Object.entries(sides)) {
|
||||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(
|
||||
pointFrom(x, y),
|
||||
side as LineSegment<Point>,
|
||||
SPACING,
|
||||
)
|
||||
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
||||
) {
|
||||
return dir as TransformHandleType;
|
||||
}
|
||||
@@ -182,9 +178,9 @@ export const getTransformHandleTypeFromCoords = <
|
||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
|
||||
const sides = getSelectionBorders(
|
||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||
pointFrom(cx, cy),
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
0 as Radians,
|
||||
);
|
||||
|
||||
@@ -192,7 +188,7 @@ export const getTransformHandleTypeFromCoords = <
|
||||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
point(scenePointerX, scenePointerY),
|
||||
side as LineSegment<Point>,
|
||||
SPACING,
|
||||
)
|
||||
@@ -269,10 +265,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
|
||||
center: Point,
|
||||
angle: Radians,
|
||||
) => {
|
||||
const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
|
||||
const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
|
||||
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
|
||||
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
|
||||
const topLeft = pointRotateRads(point(x1, y1), center, angle);
|
||||
const topRight = pointRotateRads(point(x2, y1), center, angle);
|
||||
const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
|
||||
const bottomRight = pointRotateRads(point(x2, y2), center, angle);
|
||||
|
||||
return {
|
||||
n: [topLeft, topRight],
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import { ARROW_TYPE } from "../constants";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@@ -32,8 +32,8 @@ describe("elbow arrow routing", () => {
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
||||
point(-45 - arrow.x, -100.1 - arrow.y),
|
||||
point(45 - arrow.x, 99.9 - arrow.y),
|
||||
]);
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
@@ -69,7 +69,7 @@ describe("elbow arrow routing", () => {
|
||||
y: -100.1,
|
||||
width: 90,
|
||||
height: 200,
|
||||
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
||||
points: [point(0, 0), point(90, 200)],
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(rectangle1);
|
||||
scene.insertElement(rectangle2);
|
||||
@@ -81,7 +81,7 @@ describe("elbow arrow routing", () => {
|
||||
expect(arrow.startBinding).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([
|
||||
[0, 0],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Radians } from "../../math";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
pointScaleFromOrigin,
|
||||
pointTranslate,
|
||||
vector,
|
||||
@@ -743,13 +743,13 @@ const getDonglePosition = (
|
||||
): GlobalPoint => {
|
||||
switch (heading) {
|
||||
case HEADING_UP:
|
||||
return pointFrom(p[0], bounds[1]);
|
||||
return point(p[0], bounds[1]);
|
||||
case HEADING_RIGHT:
|
||||
return pointFrom(bounds[2], p[1]);
|
||||
return point(bounds[2], p[1]);
|
||||
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 = (
|
||||
|
||||
@@ -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 type {
|
||||
ElementsMap,
|
||||
@@ -32,30 +30,6 @@ import {
|
||||
} from "./containerCache";
|
||||
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) => {
|
||||
return (
|
||||
normalizeEOL(text)
|
||||
@@ -90,12 +64,18 @@ export const redrawTextBoundingBox = (
|
||||
maxWidth = container
|
||||
? getBoundTextMaxWidth(container, textElement)
|
||||
: textElement.width;
|
||||
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
||||
boundTextUpdates.text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
}
|
||||
|
||||
const metrics = measureTextElement(textElement, {
|
||||
text: boundTextUpdates.text,
|
||||
});
|
||||
const metrics = measureText(
|
||||
boundTextUpdates.text,
|
||||
getFontString(textElement),
|
||||
textElement.lineHeight,
|
||||
);
|
||||
|
||||
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
|
||||
if (textElement.autoResize) {
|
||||
@@ -103,14 +83,6 @@ export const redrawTextBoundingBox = (
|
||||
}
|
||||
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) {
|
||||
const maxContainerHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
@@ -219,9 +191,17 @@ export const handleBindTextResize = (
|
||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
||||
) {
|
||||
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;
|
||||
nextWidth = metrics.width;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@@ -42,7 +42,7 @@ describe("textWysiwyg", () => {
|
||||
type: "line",
|
||||
width: 100,
|
||||
height: 0,
|
||||
points: [pointFrom(0, 0), pointFrom(100, 0)],
|
||||
points: [point(0, 0), point(100, 0)],
|
||||
});
|
||||
const textSize = 20;
|
||||
const text = API.createElement({
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
getContainerElement,
|
||||
getTextElementAngle,
|
||||
getTextWidth,
|
||||
measureText,
|
||||
normalizeText,
|
||||
redrawTextBoundingBox,
|
||||
wrapText,
|
||||
@@ -47,15 +46,12 @@ import {
|
||||
import type App from "../components/App";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import type { SubtypeMethods } from "./subtypes";
|
||||
import { getSubtypeMethods } from "./subtypes";
|
||||
import {
|
||||
originalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
|
||||
const getTransform = (
|
||||
offsetX: number,
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number,
|
||||
@@ -73,18 +69,9 @@ const getTransform = (
|
||||
if (height > maxHeight && zoom.value !== 1) {
|
||||
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)${offset}`;
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
};
|
||||
|
||||
const getEditorStyle = function (element) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.getEditorStyle) {
|
||||
return map.getEditorStyle(element);
|
||||
}
|
||||
return {};
|
||||
} as SubtypeMethods["getEditorStyle"];
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
@@ -150,27 +137,14 @@ export const textWysiwyg = ({
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
// Editing metrics
|
||||
const eMetrics = measureText(
|
||||
container && updatedTextElement.containerId
|
||||
? wrapText(
|
||||
updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
getBoundTextMaxWidth(container, updatedTextElement),
|
||||
)
|
||||
: updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
updatedTextElement.lineHeight,
|
||||
);
|
||||
let width = updatedTextElement.width;
|
||||
|
||||
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
|
||||
let height = Math.max(updatedTextElement.height, eMetrics.height);
|
||||
let height = updatedTextElement.height;
|
||||
|
||||
let maxWidth = width;
|
||||
let maxHeight = height;
|
||||
let maxWidth = updatedTextElement.width;
|
||||
let maxHeight = updatedTextElement.height;
|
||||
|
||||
if (container && updatedTextElement.containerId) {
|
||||
if (isArrowElement(container)) {
|
||||
@@ -266,31 +240,8 @@ export const textWysiwyg = ({
|
||||
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
|
||||
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);
|
||||
|
||||
@@ -310,9 +261,7 @@ export const textWysiwyg = ({
|
||||
height: `${height}px`,
|
||||
left: `${viewportX - padding}px`,
|
||||
top: `${viewportY}px`,
|
||||
...transformOrigin,
|
||||
transform: getTransform(
|
||||
offsetX,
|
||||
width,
|
||||
height,
|
||||
getTextElementAngle(updatedTextElement, container),
|
||||
@@ -373,7 +322,6 @@ export const textWysiwyg = ({
|
||||
whiteSpace,
|
||||
overflowWrap: "break-word",
|
||||
boxSizing: "content-box",
|
||||
...getEditorStyle(element),
|
||||
});
|
||||
editable.value = element.originalText;
|
||||
updateWysiwygStyle();
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
isIOS,
|
||||
} from "../constants";
|
||||
import type { Radians } from "../../math";
|
||||
import { pointFrom, pointRotateRads } from "../../math";
|
||||
import { point, pointRotateRads } from "../../math";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
@@ -95,8 +95,8 @@ const generateTransformHandle = (
|
||||
angle: Radians,
|
||||
): TransformHandle => {
|
||||
const [xx, yy] = pointRotateRads(
|
||||
pointFrom(x + width / 2, y + height / 2),
|
||||
pointFrom(cx, cy),
|
||||
point(x + width / 2, y + height / 2),
|
||||
point(cx, cy),
|
||||
angle,
|
||||
);
|
||||
return [xx - width / 2, yy - height / 2, width, height];
|
||||
|
||||
@@ -76,7 +76,6 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
subtype?: string;
|
||||
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 LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
||||
|
||||
import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
||||
import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
||||
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||
|
||||
import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
||||
import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
||||
import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
||||
import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
||||
import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
||||
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
||||
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
||||
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
||||
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||
|
||||
export class Fonts {
|
||||
// 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 { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||
import type { ReadonlySetLike } from "./utility-types";
|
||||
import { isPointWithinBounds, pointFrom } from "../math";
|
||||
import { isPointWithinBounds, point } from "../math";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
@@ -159,9 +159,9 @@ export const isCursorInFrame = (
|
||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||
|
||||
return isPointWithinBounds(
|
||||
pointFrom(fx1, fy1),
|
||||
pointFrom(cursorCoords.x, cursorCoords.y),
|
||||
pointFrom(fx2, fy2),
|
||||
point(fx1, fy1),
|
||||
point(cursorCoords.x, cursorCoords.y),
|
||||
point(fx2, fy2),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Vendored
-2
@@ -104,5 +104,3 @@ declare namespace jest {
|
||||
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 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) => {
|
||||
currentLang = lang;
|
||||
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);
|
||||
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);
|
||||
@@ -142,9 +123,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||
};
|
||||
|
||||
export const t = (
|
||||
path:
|
||||
| NestedKeyOf<typeof fallbackLangData>
|
||||
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
|
||||
path: NestedKeyOf<typeof fallbackLangData>,
|
||||
replacement?: { [key: string]: string | number } | null,
|
||||
fallback?: string,
|
||||
) => {
|
||||
@@ -159,7 +138,6 @@ export const t = (
|
||||
let translation =
|
||||
findPartsForData(currentLangData, parts) ||
|
||||
findPartsForData(fallbackLangData, parts) ||
|
||||
findPartsForData(fallbackCustomLangData, parts) ||
|
||||
fallback;
|
||||
if (translation === undefined) {
|
||||
const errorMessage = `Can't find translation for ${path}`;
|
||||
|
||||
@@ -230,7 +230,8 @@
|
||||
"resetLibrary": "This will clear your library. Are you sure?",
|
||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||
"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": {
|
||||
"unsupportedFileType": "Unsupported file type.",
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.13.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"mathjax-full": "3.2.2",
|
||||
"nanoid": "3.3.3",
|
||||
"open-color": "1.9.1",
|
||||
"pako": "1.0.11",
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
|
||||
import type {
|
||||
StaticCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
InteractiveCanvasRenderConfig,
|
||||
} from "../scene/types";
|
||||
import { distance, getFontString, isRTL } from "../utils";
|
||||
@@ -36,7 +37,6 @@ import type {
|
||||
PendingExcalidrawElements,
|
||||
} from "../types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { getSubtypeMethods } from "../element/subtypes";
|
||||
import {
|
||||
BOUND_TEXT_PADDING,
|
||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
@@ -251,14 +251,7 @@ const generateElementCanvas = (
|
||||
context.filter = IMAGE_INVERT_FILTER;
|
||||
}
|
||||
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
|
||||
context.restore();
|
||||
|
||||
@@ -381,22 +374,11 @@ const drawImagePlaceholder = (
|
||||
|
||||
const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
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) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
@@ -691,7 +673,7 @@ export const renderSelectionElement = (
|
||||
|
||||
export const renderElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
elementsMap: RenderableElementsMap,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
@@ -760,14 +742,7 @@ export const renderElement = (
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle);
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
context.restore();
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
@@ -862,7 +837,6 @@ export const renderElement = (
|
||||
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
tempRc,
|
||||
tempCanvasContext,
|
||||
renderConfig,
|
||||
@@ -906,14 +880,7 @@ export const renderElement = (
|
||||
}
|
||||
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
}
|
||||
|
||||
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 type { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||
import type { InteractiveCanvasAppState } from "../types";
|
||||
@@ -140,31 +140,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
||||
// (1)
|
||||
if (!appState.zenModeEnabled) {
|
||||
drawLine(
|
||||
pointFrom(from[0], from[1] - FULL),
|
||||
pointFrom(from[0], from[1] + FULL),
|
||||
point(from[0], from[1] - FULL),
|
||||
point(from[0], from[1] + FULL),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
// (3)
|
||||
drawLine(
|
||||
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
||||
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
||||
point(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
||||
point(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
||||
context,
|
||||
);
|
||||
drawLine(
|
||||
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
||||
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
||||
point(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
||||
point(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
||||
context,
|
||||
);
|
||||
|
||||
if (!appState.zenModeEnabled) {
|
||||
// (4)
|
||||
drawLine(
|
||||
pointFrom(to[0], to[1] - FULL),
|
||||
pointFrom(to[0], to[1] + FULL),
|
||||
context,
|
||||
);
|
||||
drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context);
|
||||
|
||||
// (2)
|
||||
drawLine(from, to, context);
|
||||
@@ -174,31 +170,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
||||
// (1)
|
||||
if (!appState.zenModeEnabled) {
|
||||
drawLine(
|
||||
pointFrom(from[0] - FULL, from[1]),
|
||||
pointFrom(from[0] + FULL, from[1]),
|
||||
point(from[0] - FULL, from[1]),
|
||||
point(from[0] + FULL, from[1]),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
// (3)
|
||||
drawLine(
|
||||
pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
||||
pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
||||
point(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
||||
point(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
||||
context,
|
||||
);
|
||||
drawLine(
|
||||
pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
||||
pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
||||
point(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
||||
point(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
||||
context,
|
||||
);
|
||||
|
||||
if (!appState.zenModeEnabled) {
|
||||
// (4)
|
||||
drawLine(
|
||||
pointFrom(to[0] - FULL, to[1]),
|
||||
pointFrom(to[0] + FULL, to[1]),
|
||||
context,
|
||||
);
|
||||
drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context);
|
||||
|
||||
// (2)
|
||||
drawLine(from, to, context);
|
||||
|
||||
@@ -35,7 +35,6 @@ import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
||||
import type { AppState, BinaryFiles } from "../types";
|
||||
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||
import { getSubtypeMethods } from "../element/subtypes";
|
||||
import { getVerticalOffset } from "../fonts";
|
||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||
|
||||
@@ -126,15 +125,6 @@ const renderElementToSvg = (
|
||||
root.appendChild(node);
|
||||
};
|
||||
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.renderSvg) {
|
||||
map.renderSvg(svgRoot, addToRoot, element, elementsMap, {
|
||||
offsetX,
|
||||
offsetY,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const opacity =
|
||||
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
||||
element.opacity) /
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { canChangeRoundness } from "./comparisons";
|
||||
import type { EmbedsValidationStatus } from "../types";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
pointDistance,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
@@ -408,7 +408,7 @@ export const _generateElementShape = (
|
||||
// initial position to it
|
||||
const points = element.points.length
|
||||
? element.points
|
||||
: [pointFrom<LocalPoint>(0, 0)];
|
||||
: [point<LocalPoint>(0, 0)];
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
shape = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
isPoint,
|
||||
pointFrom,
|
||||
point,
|
||||
pointDistance,
|
||||
pointFromPair,
|
||||
pointRotateRads,
|
||||
@@ -167,15 +167,15 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
? getClosedCurveShape<Point>(
|
||||
element,
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
point<Point>(element.x, element.y),
|
||||
element.angle,
|
||||
pointFrom(cx, cy),
|
||||
point(cx, cy),
|
||||
)
|
||||
: getCurveShape<Point>(
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
point<Point>(element.x, element.y),
|
||||
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);
|
||||
return getFreedrawShape(
|
||||
element,
|
||||
pointFrom(cx, cy),
|
||||
point(cx, cy),
|
||||
shouldTestInside(element),
|
||||
);
|
||||
}
|
||||
@@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = <
|
||||
}
|
||||
|
||||
const ops = getCurvePathOps(shape[0]);
|
||||
let currentP = pointFrom<P>(0, 0);
|
||||
let currentP = point<P>(0, 0);
|
||||
let index = 0;
|
||||
let minDistance = Infinity;
|
||||
let controlPoints: P[] | null = null;
|
||||
@@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = <
|
||||
}
|
||||
if (op === "bcurveTo") {
|
||||
const p0 = currentP;
|
||||
const p1 = pointFrom<P>(data[0], data[1]);
|
||||
const p2 = pointFrom<P>(data[2], data[3]);
|
||||
const p3 = pointFrom<P>(data[4], data[5]);
|
||||
const p1 = point<P>(data[0], data[1]);
|
||||
const p2 = point<P>(data[2], data[3]);
|
||||
const p3 = point<P>(data[4], data[5]);
|
||||
const distance = pointDistance(p3, endPoint);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
@@ -279,7 +279,7 @@ export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
|
||||
p0[idx] * Math.pow(t, 3);
|
||||
const tx = equation(t, 0);
|
||||
const ty = equation(t, 1);
|
||||
return pointFrom(tx, ty);
|
||||
return point(tx, ty);
|
||||
};
|
||||
|
||||
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
||||
@@ -301,12 +301,12 @@ const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
||||
controlPoints[3],
|
||||
t,
|
||||
);
|
||||
pointsOnCurve.push(pointFrom(p[0], p[1]));
|
||||
pointsOnCurve.push(point(p[0], p[1]));
|
||||
t -= 0.05;
|
||||
}
|
||||
if (pointsOnCurve.length) {
|
||||
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
||||
pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1]));
|
||||
pointsOnCurve.push(point(endPoint[0], endPoint[1]));
|
||||
}
|
||||
}
|
||||
return pointsOnCurve;
|
||||
@@ -393,24 +393,24 @@ export const aabbForElement = (
|
||||
midY: element.y + element.height / 2,
|
||||
};
|
||||
|
||||
const center = pointFrom(bbox.midX, bbox.midY);
|
||||
const center = point(bbox.midX, bbox.midY);
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(bbox.minX, bbox.minY),
|
||||
point(bbox.minX, bbox.minY),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
const [topRightX, topRightY] = pointRotateRads(
|
||||
pointFrom(bbox.maxX, bbox.minY),
|
||||
point(bbox.maxX, bbox.minY),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
const [bottomRightX, bottomRightY] = pointRotateRads(
|
||||
pointFrom(bbox.maxX, bbox.maxY),
|
||||
point(bbox.maxX, bbox.maxY),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
||||
pointFrom(bbox.minX, bbox.maxY),
|
||||
point(bbox.minX, bbox.maxY),
|
||||
center,
|
||||
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];
|
||||
|
||||
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
||||
pointInsideBounds(pointFrom(a[0], a[1]), b) ||
|
||||
pointInsideBounds(pointFrom(a[2], a[1]), b) ||
|
||||
pointInsideBounds(pointFrom(a[2], a[3]), b) ||
|
||||
pointInsideBounds(pointFrom(a[0], a[3]), b) ||
|
||||
pointInsideBounds(pointFrom(b[0], b[1]), a) ||
|
||||
pointInsideBounds(pointFrom(b[2], b[1]), a) ||
|
||||
pointInsideBounds(pointFrom(b[2], b[3]), a) ||
|
||||
pointInsideBounds(pointFrom(b[0], b[3]), a);
|
||||
pointInsideBounds(point(a[0], a[1]), b) ||
|
||||
pointInsideBounds(point(a[2], a[1]), b) ||
|
||||
pointInsideBounds(point(a[2], a[3]), b) ||
|
||||
pointInsideBounds(point(a[0], a[3]), b) ||
|
||||
pointInsideBounds(point(b[0], b[1]), a) ||
|
||||
pointInsideBounds(point(b[2], b[1]), a) ||
|
||||
pointInsideBounds(point(b[2], b[3]), a) ||
|
||||
pointInsideBounds(point(b[0], b[3]), a);
|
||||
|
||||
export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InclusiveRange } from "../math";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
pointRotateRads,
|
||||
rangeInclusive,
|
||||
rangeIntersection,
|
||||
@@ -228,52 +228,52 @@ export const getElementsCorners = (
|
||||
!boundingBoxCorners
|
||||
) {
|
||||
const leftMid = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x1, y1 + halfHeight),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, y1 + halfHeight),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const topMid = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x1 + halfWidth, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(x1 + halfWidth, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const rightMid = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x2, y1 + halfHeight),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, y1 + halfHeight),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const bottomMid = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x1 + halfWidth, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(x1 + halfWidth, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||
const center = point<GlobalPoint>(cx, cy);
|
||||
|
||||
result = omitCenter
|
||||
? [leftMid, topMid, rightMid, bottomMid]
|
||||
: [leftMid, topMid, rightMid, bottomMid, center];
|
||||
} else {
|
||||
const topLeft = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const topRight = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x2, y1),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, y1),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const bottomLeft = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x1, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(x1, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const bottomRight = pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x2, y2),
|
||||
pointFrom(cx, cy),
|
||||
point(x2, y2),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||
const center = point<GlobalPoint>(cx, cy);
|
||||
|
||||
result = omitCenter
|
||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||
@@ -287,18 +287,18 @@ export const getElementsCorners = (
|
||||
const width = maxX - minX;
|
||||
const height = maxY - minY;
|
||||
|
||||
const topLeft = pointFrom<GlobalPoint>(minX, minY);
|
||||
const topRight = pointFrom<GlobalPoint>(maxX, minY);
|
||||
const bottomLeft = pointFrom<GlobalPoint>(minX, maxY);
|
||||
const bottomRight = pointFrom<GlobalPoint>(maxX, maxY);
|
||||
const center = pointFrom<GlobalPoint>(minX + width / 2, minY + height / 2);
|
||||
const topLeft = point<GlobalPoint>(minX, minY);
|
||||
const topRight = point<GlobalPoint>(maxX, minY);
|
||||
const bottomLeft = point<GlobalPoint>(minX, maxY);
|
||||
const bottomRight = point<GlobalPoint>(maxX, maxY);
|
||||
const center = point<GlobalPoint>(minX + width / 2, minY + height / 2);
|
||||
|
||||
result = omitCenter
|
||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||
: [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 = (
|
||||
@@ -375,11 +375,8 @@ export const getVisibleGaps = (
|
||||
horizontalGaps.push({
|
||||
startBounds,
|
||||
endBounds,
|
||||
startSide: [
|
||||
pointFrom(startMaxX, startMinY),
|
||||
pointFrom(startMaxX, startMaxY),
|
||||
],
|
||||
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)],
|
||||
startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
|
||||
endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
|
||||
length: endMinX - startMaxX,
|
||||
overlap: rangeIntersection(
|
||||
rangeInclusive(startMinY, startMaxY),
|
||||
@@ -418,11 +415,8 @@ export const getVisibleGaps = (
|
||||
verticalGaps.push({
|
||||
startBounds,
|
||||
endBounds,
|
||||
startSide: [
|
||||
pointFrom(startMinX, startMaxY),
|
||||
pointFrom(startMaxX, startMaxY),
|
||||
],
|
||||
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)],
|
||||
startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
|
||||
endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
|
||||
length: endMinY - startMaxY,
|
||||
overlap: rangeIntersection(
|
||||
rangeInclusive(startMinX, startMaxX),
|
||||
@@ -838,7 +832,7 @@ const createPointSnapLines = (
|
||||
}
|
||||
snapsX[key].push(
|
||||
...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(
|
||||
...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
|
||||
.map((p) => {
|
||||
return pointFrom<GlobalPoint>(Number(key), p[1]);
|
||||
return point<GlobalPoint>(Number(key), p[1]);
|
||||
})
|
||||
.sort((a, b) => a[1] - b[1]),
|
||||
),
|
||||
@@ -882,7 +876,7 @@ const createPointSnapLines = (
|
||||
points: dedupePoints(
|
||||
points
|
||||
.map((p) => {
|
||||
return pointFrom<GlobalPoint>(p[0], Number(key));
|
||||
return point<GlobalPoint>(p[0], Number(key));
|
||||
})
|
||||
.sort((a, b) => a[0] - b[0]),
|
||||
),
|
||||
@@ -946,16 +940,16 @@ const createGapSnapLines = (
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [
|
||||
pointFrom(gapSnap.gap.startSide[0][0], gapLineY),
|
||||
pointFrom(minX, gapLineY),
|
||||
point(gapSnap.gap.startSide[0][0], gapLineY),
|
||||
point(minX, gapLineY),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [
|
||||
pointFrom(maxX, gapLineY),
|
||||
pointFrom(gapSnap.gap.endSide[0][0], gapLineY),
|
||||
point(maxX, gapLineY),
|
||||
point(gapSnap.gap.endSide[0][0], gapLineY),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -972,16 +966,16 @@ const createGapSnapLines = (
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [
|
||||
pointFrom(gapLineX, gapSnap.gap.startSide[0][1]),
|
||||
pointFrom(gapLineX, minY),
|
||||
point(gapLineX, gapSnap.gap.startSide[0][1]),
|
||||
point(gapLineX, minY),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [
|
||||
pointFrom(gapLineX, maxY),
|
||||
pointFrom(gapLineX, gapSnap.gap.endSide[0][1]),
|
||||
point(gapLineX, maxY),
|
||||
point(gapLineX, gapSnap.gap.endSide[0][1]),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -997,15 +991,12 @@ const createGapSnapLines = (
|
||||
{
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [
|
||||
pointFrom(startMaxX, gapLineY),
|
||||
pointFrom(endMinX, gapLineY),
|
||||
],
|
||||
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)],
|
||||
points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1020,18 +1011,12 @@ const createGapSnapLines = (
|
||||
{
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [
|
||||
pointFrom(maxX, gapLineY),
|
||||
pointFrom(startMinX, gapLineY),
|
||||
],
|
||||
points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "horizontal",
|
||||
points: [
|
||||
pointFrom(startMaxX, gapLineY),
|
||||
pointFrom(endMinX, gapLineY),
|
||||
],
|
||||
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1046,18 +1031,12 @@ const createGapSnapLines = (
|
||||
{
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [
|
||||
pointFrom(gapLineX, maxY),
|
||||
pointFrom(gapLineX, startMinY),
|
||||
],
|
||||
points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [
|
||||
pointFrom(gapLineX, startMaxY),
|
||||
pointFrom(gapLineX, endMinY),
|
||||
],
|
||||
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1072,15 +1051,12 @@ const createGapSnapLines = (
|
||||
{
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [
|
||||
pointFrom(gapLineX, startMaxY),
|
||||
pointFrom(gapLineX, endMinY),
|
||||
],
|
||||
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||
},
|
||||
{
|
||||
type: "gap",
|
||||
direction: "vertical",
|
||||
points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)],
|
||||
points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1094,7 +1070,7 @@ const createGapSnapLines = (
|
||||
return {
|
||||
...gapSnapLine,
|
||||
points: gapSnapLine.points.map((p) =>
|
||||
pointFrom(round(p[0]), round(p[1])),
|
||||
point(round(p[0]), round(p[1])),
|
||||
) as PointPair,
|
||||
};
|
||||
}),
|
||||
@@ -1144,35 +1120,35 @@ export const snapResizingElements = (
|
||||
if (transformHandle) {
|
||||
switch (transformHandle) {
|
||||
case "e": {
|
||||
selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY));
|
||||
selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
|
||||
break;
|
||||
}
|
||||
case "w": {
|
||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY));
|
||||
selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
|
||||
break;
|
||||
}
|
||||
case "n": {
|
||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY));
|
||||
selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
|
||||
break;
|
||||
}
|
||||
case "s": {
|
||||
selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY));
|
||||
selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
|
||||
break;
|
||||
}
|
||||
case "ne": {
|
||||
selectionSnapPoints.push(pointFrom(maxX, minY));
|
||||
selectionSnapPoints.push(point(maxX, minY));
|
||||
break;
|
||||
}
|
||||
case "nw": {
|
||||
selectionSnapPoints.push(pointFrom(minX, minY));
|
||||
selectionSnapPoints.push(point(minX, minY));
|
||||
break;
|
||||
}
|
||||
case "se": {
|
||||
selectionSnapPoints.push(pointFrom(maxX, maxY));
|
||||
selectionSnapPoints.push(point(maxX, maxY));
|
||||
break;
|
||||
}
|
||||
case "sw": {
|
||||
selectionSnapPoints.push(pointFrom(minX, maxY));
|
||||
selectionSnapPoints.push(point(minX, maxY));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1215,10 +1191,10 @@ export const snapResizingElements = (
|
||||
);
|
||||
|
||||
const corners: GlobalPoint[] = [
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(x1, y2),
|
||||
pointFrom(x2, y1),
|
||||
pointFrom(x2, y2),
|
||||
point(x1, y1),
|
||||
point(x1, y2),
|
||||
point(x2, y1),
|
||||
point(x2, y2),
|
||||
];
|
||||
|
||||
getPointSnaps(
|
||||
@@ -1255,7 +1231,7 @@ export const snapNewElement = (
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1355,7 +1331,7 @@ export const getSnapLinesAtPointer = (
|
||||
|
||||
verticalSnapLines.push({
|
||||
type: "pointer",
|
||||
points: [corner, pointFrom(corner[0], pointer.y)],
|
||||
points: [corner, point(corner[0], pointer.y)],
|
||||
direction: "vertical",
|
||||
});
|
||||
|
||||
@@ -1371,7 +1347,7 @@ export const getSnapLinesAtPointer = (
|
||||
|
||||
horizontalSnapLines.push({
|
||||
type: "pointer",
|
||||
points: [corner, pointFrom(pointer.x, corner[1])],
|
||||
points: [corner, point(pointer.x, corner[1])],
|
||||
direction: "horizontal",
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { API } from "./helpers/api";
|
||||
import { KEYS } from "../keys";
|
||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@@ -32,12 +32,7 @@ describe("element binding", () => {
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 1,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(100, 0),
|
||||
pointFrom(100, 0),
|
||||
],
|
||||
points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)],
|
||||
});
|
||||
API.setElements([rect, arrow]);
|
||||
expect(arrow.startBinding).toBe(null);
|
||||
@@ -315,7 +310,7 @@ describe("element binding", () => {
|
||||
const arrow1 = API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow1",
|
||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||
startBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
@@ -333,7 +328,7 @@ describe("element binding", () => {
|
||||
const arrow2 = API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow2",
|
||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||
startBinding: {
|
||||
elementId: "text1",
|
||||
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 { arrayToMap, cloneJSON } from "../utils";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom, type Radians } from "../../math";
|
||||
import { point, type Radians } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
const mouse = new Pointer("mouse");
|
||||
@@ -146,9 +146,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
|
||||
link: null,
|
||||
locked: false,
|
||||
points: [
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
||||
pointFrom<LocalPoint>(828.0126953125, 410.51605224609375),
|
||||
point<LocalPoint>(0, 0),
|
||||
point<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
||||
point<LocalPoint>(828.0126953125, 410.51605224609375),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,18 +20,7 @@ import fs from "fs";
|
||||
import util from "util";
|
||||
import path from "path";
|
||||
import { getMimeType } from "../../data/blob";
|
||||
import type {
|
||||
SubtypeLoadedCb,
|
||||
SubtypePrepFn,
|
||||
SubtypeRecord,
|
||||
} from "../../element/subtypes";
|
||||
import {
|
||||
checkRefreshOnSubtypeLoad,
|
||||
prepareSubtype,
|
||||
selectSubtype,
|
||||
} from "../../element/subtypes";
|
||||
import {
|
||||
maybeGetSubtypeProps,
|
||||
newArrowElement,
|
||||
newEmbeddableElement,
|
||||
newFrameElement,
|
||||
@@ -49,7 +38,7 @@ import type App from "../../components/App";
|
||||
import { createTestHook } from "../../components/App";
|
||||
import type { Action } from "../../actions/types";
|
||||
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);
|
||||
// so that window.h is available when App.tsx is not imported as well.
|
||||
@@ -58,19 +47,6 @@ createTestHook();
|
||||
const { h } = window;
|
||||
|
||||
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) => {
|
||||
act(() => {
|
||||
h.app.updateScene(...args);
|
||||
@@ -199,8 +175,6 @@ export class API {
|
||||
verticalAlign?: T extends "text"
|
||||
? ExcalidrawTextElement["verticalAlign"]
|
||||
: never;
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
boundElements?: ExcalidrawGenericElement["boundElements"];
|
||||
containerId?: T extends "text"
|
||||
? ExcalidrawTextElement["containerId"]
|
||||
@@ -240,14 +214,6 @@ export class API {
|
||||
|
||||
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<
|
||||
ExcalidrawGenericElement,
|
||||
| "id"
|
||||
@@ -262,7 +228,6 @@ export class API {
|
||||
| "link"
|
||||
| "updated"
|
||||
> = {
|
||||
...custom,
|
||||
x,
|
||||
y,
|
||||
frameId: rest.frameId ?? null,
|
||||
@@ -342,8 +307,8 @@ export class API {
|
||||
height,
|
||||
type,
|
||||
points: rest.points ?? [
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(100, 100),
|
||||
point<LocalPoint>(0, 0),
|
||||
point<LocalPoint>(100, 100),
|
||||
],
|
||||
elbowed: rest.elbowed ?? false,
|
||||
});
|
||||
@@ -355,8 +320,8 @@ export class API {
|
||||
height,
|
||||
type,
|
||||
points: rest.points ?? [
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(100, 100),
|
||||
point<LocalPoint>(0, 0),
|
||||
point<LocalPoint>(100, 100),
|
||||
],
|
||||
});
|
||||
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 { createTestHook } from "../../components/App";
|
||||
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.
|
||||
createTestHook();
|
||||
@@ -142,7 +142,7 @@ const getElementPointForSelection = (
|
||||
element: ExcalidrawElement,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const target = pointFrom<GlobalPoint>(
|
||||
const target = point<GlobalPoint>(
|
||||
x +
|
||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||
y,
|
||||
@@ -151,12 +151,9 @@ const getElementPointForSelection = (
|
||||
|
||||
if (isLinearElement(element)) {
|
||||
const bounds = getElementPointsCoords(element, element.points);
|
||||
center = pointFrom(
|
||||
(bounds[0] + bounds[2]) / 2,
|
||||
(bounds[1] + bounds[3]) / 2,
|
||||
);
|
||||
center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2);
|
||||
} else {
|
||||
center = pointFrom(x + width / 2, y + height / 2);
|
||||
center = point(x + width / 2, y + height / 2);
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
@@ -472,8 +469,8 @@ export class UI {
|
||||
const width = initialWidth ?? initialHeight ?? size;
|
||||
const height = initialHeight ?? size;
|
||||
const points: LocalPoint[] = initialPoints ?? [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(width, height),
|
||||
point(0, 0),
|
||||
point(width, height),
|
||||
];
|
||||
|
||||
UI.clickTool(type);
|
||||
|
||||
@@ -46,7 +46,7 @@ import { HistoryEntry } from "../history";
|
||||
import { AppStateChange, ElementsChange } from "../change";
|
||||
import { Snapshot, StoreAction } from "../store";
|
||||
import type { LocalPoint, Radians } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@@ -2041,9 +2041,9 @@ describe("history", () => {
|
||||
width: 178.9000000000001,
|
||||
height: 236.10000000000002,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(178.9000000000001, 0),
|
||||
pointFrom(178.9000000000001, 236.10000000000002),
|
||||
point(0, 0),
|
||||
point(178.9000000000001, 0),
|
||||
point(178.9000000000001, 236.10000000000002),
|
||||
],
|
||||
startBinding: {
|
||||
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
||||
@@ -2159,11 +2159,11 @@ describe("history", () => {
|
||||
elements: [
|
||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(5, 5),
|
||||
pointFrom(10, 10),
|
||||
pointFrom(15, 15),
|
||||
pointFrom(20, 20),
|
||||
point(0, 0),
|
||||
point(5, 5),
|
||||
point(10, 10),
|
||||
point(15, 15),
|
||||
point(20, 20),
|
||||
] as LocalPoint[],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||
import { vi } from "vitest";
|
||||
import { arrayToMap } from "../utils";
|
||||
import type { GlobalPoint } from "../../math";
|
||||
import { pointCenter, pointFrom } from "../../math";
|
||||
import { pointCenter, point } from "../../math";
|
||||
|
||||
const renderInteractiveScene = vi.spyOn(
|
||||
InteractiveCanvas,
|
||||
@@ -57,8 +57,8 @@ describe("Test Linear Elements", () => {
|
||||
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||
});
|
||||
|
||||
const p1 = pointFrom<GlobalPoint>(20, 20);
|
||||
const p2 = pointFrom<GlobalPoint>(60, 20);
|
||||
const p1 = point<GlobalPoint>(20, 20);
|
||||
const p2 = point<GlobalPoint>(60, 20);
|
||||
const midpoint = pointCenter<GlobalPoint>(p1, p2);
|
||||
const delta = 50;
|
||||
const mouse = new Pointer("mouse");
|
||||
@@ -75,7 +75,7 @@ describe("Test Linear Elements", () => {
|
||||
height: 0,
|
||||
type,
|
||||
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,
|
||||
});
|
||||
API.setElements([line]);
|
||||
@@ -99,9 +99,9 @@ describe("Test Linear Elements", () => {
|
||||
type,
|
||||
roughness,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(p3[0], p3[1]),
|
||||
pointFrom(p2[0] - p1[0], p2[1] - p1[1]),
|
||||
point(0, 0),
|
||||
point(p3[0], p3[1]),
|
||||
point(p2[0] - p1[0], p2[1] - p1[1]),
|
||||
],
|
||||
roundness,
|
||||
});
|
||||
@@ -161,7 +161,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(line.points.length).toEqual(2);
|
||||
|
||||
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);
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(line.y).toBe(originalY);
|
||||
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.y).toBe(originalY);
|
||||
expect(line.points.length).toEqual(3);
|
||||
@@ -184,7 +184,7 @@ describe("Test Linear Elements", () => {
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||
|
||||
// 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(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(line.points.length).toEqual(3);
|
||||
@@ -248,7 +248,7 @@ describe("Test Linear Elements", () => {
|
||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||
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.y).toBe(originalY);
|
||||
expect(line.points.length).toEqual(3);
|
||||
@@ -261,7 +261,7 @@ describe("Test Linear Elements", () => {
|
||||
enterLineEditingMode(line);
|
||||
|
||||
// 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(
|
||||
`12`,
|
||||
);
|
||||
@@ -356,7 +356,7 @@ describe("Test Linear Elements", () => {
|
||||
const startPoint = pointCenter(points[0], midPoints[0]!);
|
||||
const deltaX = 50;
|
||||
const deltaY = 20;
|
||||
const endPoint = pointFrom<GlobalPoint>(
|
||||
const endPoint = point<GlobalPoint>(
|
||||
startPoint[0] + deltaX,
|
||||
startPoint[1] + deltaY,
|
||||
);
|
||||
@@ -399,8 +399,8 @@ describe("Test Linear Elements", () => {
|
||||
// This is the expected midpoint for line with round edge
|
||||
// hence hardcoding it so if later some bug is introduced
|
||||
// this will fail and we can fix it
|
||||
const firstSegmentMidpoint = pointFrom<GlobalPoint>(55, 45);
|
||||
const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40);
|
||||
const firstSegmentMidpoint = point<GlobalPoint>(55, 45);
|
||||
const lastSegmentMidpoint = point<GlobalPoint>(75, 40);
|
||||
|
||||
let line: ExcalidrawLinearElement;
|
||||
|
||||
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
|
||||
// drag line via first segment midpoint
|
||||
drag(
|
||||
firstSegmentMidpoint,
|
||||
pointFrom(
|
||||
point(
|
||||
firstSegmentMidpoint[0] + delta,
|
||||
firstSegmentMidpoint[1] + delta,
|
||||
),
|
||||
@@ -426,10 +426,7 @@ describe("Test Linear Elements", () => {
|
||||
// drag line from last segment midpoint
|
||||
drag(
|
||||
lastSegmentMidpoint,
|
||||
pointFrom(
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
),
|
||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||
);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
@@ -478,10 +475,10 @@ describe("Test Linear Elements", () => {
|
||||
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(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
@@ -519,10 +516,10 @@ describe("Test Linear Elements", () => {
|
||||
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(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
@@ -559,7 +556,7 @@ describe("Test Linear Elements", () => {
|
||||
// dragging line from last segment midpoint
|
||||
drag(
|
||||
lastSegmentMidpoint,
|
||||
pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
||||
point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
||||
);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
@@ -592,11 +589,11 @@ describe("Test Linear Elements", () => {
|
||||
// This is the expected midpoint for line with round edge
|
||||
// hence hardcoding it so if later some bug is introduced
|
||||
// this will fail and we can fix it
|
||||
const firstSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||
const firstSegmentMidpoint = point<GlobalPoint>(
|
||||
55.9697848965255,
|
||||
47.442326230998205,
|
||||
);
|
||||
const lastSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||
const lastSegmentMidpoint = point<GlobalPoint>(
|
||||
76.08587175006699,
|
||||
43.294165939653226,
|
||||
);
|
||||
@@ -615,7 +612,7 @@ describe("Test Linear Elements", () => {
|
||||
// drag line from first segment midpoint
|
||||
drag(
|
||||
firstSegmentMidpoint,
|
||||
pointFrom(
|
||||
point(
|
||||
firstSegmentMidpoint[0] + delta,
|
||||
firstSegmentMidpoint[1] + delta,
|
||||
),
|
||||
@@ -625,10 +622,7 @@ describe("Test Linear Elements", () => {
|
||||
// drag line from last segment midpoint
|
||||
drag(
|
||||
lastSegmentMidpoint,
|
||||
pointFrom(
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
),
|
||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||
);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`16`,
|
||||
@@ -675,10 +669,10 @@ describe("Test Linear Elements", () => {
|
||||
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(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -723,10 +717,10 @@ describe("Test Linear Elements", () => {
|
||||
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(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
@@ -757,10 +751,7 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
drag(
|
||||
lastSegmentMidpoint,
|
||||
pointFrom(
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
),
|
||||
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||
);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
@@ -820,8 +811,8 @@ describe("Test Linear Elements", () => {
|
||||
API.setSelectedElements([line]);
|
||||
enterLineEditingMode(line, true);
|
||||
drag(
|
||||
pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
||||
pointFrom(
|
||||
point(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
||||
point(
|
||||
dragEndPositionOffset[0] + line.x,
|
||||
dragEndPositionOffset[1] + line.y,
|
||||
),
|
||||
@@ -936,14 +927,14 @@ describe("Test Linear Elements", () => {
|
||||
// This is the expected midpoint for line with round edge
|
||||
// hence hardcoding it so if later some bug is introduced
|
||||
// this will fail and we can fix it
|
||||
const firstSegmentMidpoint = pointFrom<GlobalPoint>(
|
||||
const firstSegmentMidpoint = point<GlobalPoint>(
|
||||
55.9697848965255,
|
||||
47.442326230998205,
|
||||
);
|
||||
// drag line from first segment midpoint
|
||||
drag(
|
||||
firstSegmentMidpoint,
|
||||
pointFrom(
|
||||
point(
|
||||
firstSegmentMidpoint[0] + delta,
|
||||
firstSegmentMidpoint[1] + delta,
|
||||
),
|
||||
@@ -1160,7 +1151,7 @@ describe("Test Linear Elements", () => {
|
||||
);
|
||||
|
||||
// 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 })
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -1359,11 +1350,11 @@ describe("Test Linear Elements", () => {
|
||||
[
|
||||
{
|
||||
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,
|
||||
point: pointFrom(
|
||||
point: point(
|
||||
line.points[line.points.length - 1][0] - 10,
|
||||
line.points[line.points.length - 1][1] - 10,
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ import { isLinearElement } from "../element/typeChecks";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { arrayToMap } from "../utils";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
@@ -220,17 +220,12 @@ describe("generic element", () => {
|
||||
|
||||
describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
||||
const points: Record<typeof type, LocalPoint[]> = {
|
||||
line: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(60, -20),
|
||||
pointFrom(20, 40),
|
||||
pointFrom(-40, 0),
|
||||
],
|
||||
line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)],
|
||||
freedraw: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(-2.474600807561444, 41.021700699972),
|
||||
pointFrom(3.6627956000014024, 47.84174560617245),
|
||||
pointFrom(40.495224145598115, 47.15909710753482),
|
||||
point(0, 0),
|
||||
point(-2.474600807561444, 41.021700699972),
|
||||
point(3.6627956000014024, 47.84174560617245),
|
||||
point(40.495224145598115, 47.15909710753482),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -298,11 +293,11 @@ describe("arrow element", () => {
|
||||
it("resizes with a label", async () => {
|
||||
const arrow = UI.createElement("arrow", {
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(40, 140),
|
||||
pointFrom(80, 60), // label's anchor
|
||||
pointFrom(180, 20),
|
||||
pointFrom(200, 120),
|
||||
point(0, 0),
|
||||
point(40, 140),
|
||||
point(80, 60), // label's anchor
|
||||
point(180, 20),
|
||||
point(200, 120),
|
||||
],
|
||||
});
|
||||
const label = await UI.editText(arrow, "Hello");
|
||||
@@ -752,24 +747,24 @@ describe("multiple selection", () => {
|
||||
x: 60,
|
||||
y: 40,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(-40, 40),
|
||||
pointFrom(-60, 0),
|
||||
pointFrom(0, -40),
|
||||
pointFrom(40, 20),
|
||||
pointFrom(0, 40),
|
||||
point(0, 0),
|
||||
point(-40, 40),
|
||||
point(-60, 0),
|
||||
point(0, -40),
|
||||
point(40, 20),
|
||||
point(0, 40),
|
||||
],
|
||||
});
|
||||
const freedraw = UI.createElement("freedraw", {
|
||||
x: 63.56072661326618,
|
||||
y: 100,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(-43.56072661326618, 18.15048126846341),
|
||||
pointFrom(-43.56072661326618, 29.041198460587566),
|
||||
pointFrom(-38.115368017204105, 42.652452795512204),
|
||||
pointFrom(-19.964886748740696, 66.24829266003775),
|
||||
pointFrom(19.056612930986716, 77.1390098521619),
|
||||
point(0, 0),
|
||||
point(-43.56072661326618, 18.15048126846341),
|
||||
point(-43.56072661326618, 29.041198460587566),
|
||||
point(-38.115368017204105, 42.652452795512204),
|
||||
point(-19.964886748740696, 66.24829266003775),
|
||||
point(19.056612930986716, 77.1390098521619),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1106,13 +1101,13 @@ describe("multiple selection", () => {
|
||||
x: 60,
|
||||
y: 0,
|
||||
points: [
|
||||
pointFrom(0, 0),
|
||||
pointFrom(-40, 40),
|
||||
pointFrom(-20, 60),
|
||||
pointFrom(20, 20),
|
||||
pointFrom(40, 40),
|
||||
pointFrom(-20, 100),
|
||||
pointFrom(-60, 60),
|
||||
point(0, 0),
|
||||
point(-40, 40),
|
||||
point(-20, 60),
|
||||
point(20, 20),
|
||||
point(40, 40),
|
||||
point(-20, 100),
|
||||
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 { MaybeTransformHandleType } from "./element/transformHandles";
|
||||
import type Library from "./data/library";
|
||||
import type {
|
||||
SubtypeMethods,
|
||||
Subtype,
|
||||
SubtypePrepFn,
|
||||
SubtypeRecord,
|
||||
} from "./element/subtypes";
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||
import type { ContextMenuItems } from "./components/ContextMenu";
|
||||
@@ -277,10 +271,6 @@ export interface AppState {
|
||||
*/
|
||||
editingTextElement: NonDeletedExcalidrawElement | null;
|
||||
editingLinearElement: LinearElementEditor | null;
|
||||
activeSubtypes?: Subtype[];
|
||||
customData?: {
|
||||
[subtype: Subtype]: ExcalidrawElement["customData"];
|
||||
};
|
||||
activeTool: {
|
||||
/**
|
||||
* 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"];
|
||||
scrollToContent: InstanceType<typeof App>["scrollToContent"];
|
||||
registerAction: (action: Action) => void;
|
||||
addSubtype: (
|
||||
record: SubtypeRecord,
|
||||
subtypePrepFn: SubtypePrepFn,
|
||||
) => { actions: readonly Action[] | null; methods: Partial<SubtypeMethods> };
|
||||
refresh: InstanceType<typeof App>["refresh"];
|
||||
setToast: InstanceType<typeof App>["setToast"];
|
||||
addFiles: (data: BinaryFileData[]) => void;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
isLineSegment,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
type GlobalPoint,
|
||||
} from "../math";
|
||||
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
|
||||
import type { LineSegment } from "../utils";
|
||||
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||
import { isBounds } from "./element/typeChecks";
|
||||
@@ -57,8 +52,8 @@ export const debugDrawPoint = (
|
||||
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<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),
|
||||
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
||||
),
|
||||
{
|
||||
color: opts?.color ?? "cyan",
|
||||
@@ -67,8 +62,8 @@ export const debugDrawPoint = (
|
||||
);
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<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),
|
||||
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
||||
),
|
||||
{
|
||||
color: opts?.color ?? "cyan",
|
||||
@@ -88,20 +83,20 @@ export const debugDrawBoundingBox = (
|
||||
debugDrawLine(
|
||||
[
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
||||
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||
),
|
||||
],
|
||||
{
|
||||
@@ -123,20 +118,20 @@ export const debugDrawBounds = (
|
||||
debugDrawLine(
|
||||
[
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||
),
|
||||
],
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isPointOnSymmetricArc } from "./arc";
|
||||
import { pointFrom } from "./point";
|
||||
import { point } from "./point";
|
||||
|
||||
describe("point on arc", () => {
|
||||
it("should detect point on simple arc", () => {
|
||||
@@ -10,7 +10,7 @@ describe("point on arc", () => {
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
pointFrom(0.92291667, 0.385),
|
||||
point(0.92291667, 0.385),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -22,7 +22,7 @@ describe("point on arc", () => {
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
pointFrom(-0.92291667, 0.385),
|
||||
point(-0.92291667, 0.385),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe("point on arc", () => {
|
||||
startAngle: -Math.PI / 4,
|
||||
endAngle: Math.PI / 4,
|
||||
},
|
||||
pointFrom(-0.5, 0.5),
|
||||
point(-0.5, 0.5),
|
||||
),
|
||||
).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";
|
||||
|
||||
/**
|
||||
@@ -43,10 +43,10 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
||||
const out: Point[] = [];
|
||||
if (len === 3) {
|
||||
out.push(
|
||||
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||
pointFrom(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[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||
point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
);
|
||||
} else {
|
||||
const points: Point[] = [];
|
||||
@@ -59,19 +59,19 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
||||
}
|
||||
const b: Point[] = [];
|
||||
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++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
|
||||
b[1] = pointFrom(
|
||||
b[0] = point(cachedVertArray[0], cachedVertArray[1]);
|
||||
b[1] = point(
|
||||
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,
|
||||
);
|
||||
b[2] = pointFrom(
|
||||
b[2] = point(
|
||||
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,
|
||||
);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
3 * (1 - t) * Math.pow(t, 2) * p2[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";
|
||||
|
||||
describe("rotate", () => {
|
||||
@@ -9,14 +9,14 @@ describe("rotate", () => {
|
||||
const y2 = 30;
|
||||
const angle = (Math.PI / 2) as Radians;
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(x2, y2),
|
||||
point(x1, y1),
|
||||
point(x2, y2),
|
||||
angle,
|
||||
);
|
||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||
const res2 = pointRotateRads(
|
||||
pointFrom(rotatedX, rotatedY),
|
||||
pointFrom(x2, y2),
|
||||
point(rotatedX, rotatedY),
|
||||
point(x2, y2),
|
||||
-angle as Radians,
|
||||
);
|
||||
expect(res2).toEqual([x1, x2]);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
|
||||
* @param y The Y coordinate
|
||||
* @returns The branded and created point
|
||||
*/
|
||||
export function pointFrom<Point extends GlobalPoint | LocalPoint>(
|
||||
export function point<Point extends GlobalPoint | LocalPoint>(
|
||||
x: number,
|
||||
y: number,
|
||||
): Point {
|
||||
@@ -33,7 +33,7 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
|
||||
numberArray: number[],
|
||||
): Point | undefined {
|
||||
return numberArray.length === 2
|
||||
? pointFrom<Point>(numberArray[0], numberArray[1])
|
||||
? point<Point>(numberArray[0], numberArray[1])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
|
||||
[cx, cy]: Point,
|
||||
angle: Radians,
|
||||
): Point {
|
||||
return pointFrom(
|
||||
return point(
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||
);
|
||||
@@ -146,7 +146,7 @@ export function pointTranslate<
|
||||
From extends GlobalPoint | LocalPoint,
|
||||
To extends GlobalPoint | LocalPoint,
|
||||
>(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
|
||||
*/
|
||||
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,
|
||||
b: 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,
|
||||
b: 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,
|
||||
lineSegment,
|
||||
lineSegmentRotate,
|
||||
pointFrom,
|
||||
point,
|
||||
pointRotateDegs,
|
||||
} from "../math";
|
||||
import { pointOnCurve, pointOnPolyline } from "./collision";
|
||||
@@ -12,21 +12,21 @@ import type { Polyline } from "./geometry/shape";
|
||||
|
||||
describe("point and curve", () => {
|
||||
const c: Curve<GlobalPoint> = curve(
|
||||
pointFrom(1.4, 1.65),
|
||||
pointFrom(1.9, 7.9),
|
||||
pointFrom(5.9, 1.65),
|
||||
pointFrom(6.44, 4.84),
|
||||
point(1.4, 1.65),
|
||||
point(1.9, 7.9),
|
||||
point(5.9, 1.65),
|
||||
point(6.44, 4.84),
|
||||
);
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(c[0], 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(pointFrom(4, 4.4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(pointFrom(5.6, 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[2], c, 0.1)).toBe(false);
|
||||
});
|
||||
@@ -34,52 +34,52 @@ describe("point and curve", () => {
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline<GlobalPoint> = [
|
||||
lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
|
||||
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
|
||||
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
|
||||
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
|
||||
lineSegment(point(1, 0), point(1, 2)),
|
||||
lineSegment(point(1, 2), point(2, 2)),
|
||||
lineSegment(point(2, 2), point(2, 1)),
|
||||
lineSegment(point(2, 1), point(3, 1)),
|
||||
];
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
pointFrom(1, 0),
|
||||
pointFrom(1, 2),
|
||||
pointFrom(2, 2),
|
||||
pointFrom(2, 1),
|
||||
pointFrom(3, 1),
|
||||
point(1, 0),
|
||||
point(1, 2),
|
||||
point(2, 2),
|
||||
point(2, 1),
|
||||
point(3, 1),
|
||||
];
|
||||
|
||||
truePoints.forEach((p) => {
|
||||
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) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||
);
|
||||
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) => {
|
||||
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) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import type { Curve } from "../math";
|
||||
import {
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
point,
|
||||
polygonIncludesPoint,
|
||||
pointOnLineSegment,
|
||||
pointOnPolygon,
|
||||
@@ -110,7 +110,7 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
for (let i = 0; i < segments; i++) {
|
||||
t += increment;
|
||||
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));
|
||||
startingPoint = nextPoint;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
||||
import {
|
||||
pointFrom,
|
||||
point,
|
||||
lineSegment,
|
||||
polygon,
|
||||
pointOnLineSegment,
|
||||
@@ -23,127 +23,93 @@ describe("point and line", () => {
|
||||
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
||||
// });
|
||||
|
||||
const s: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 0),
|
||||
pointFrom(1, 2),
|
||||
);
|
||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
|
||||
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
|
||||
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
|
||||
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
|
||||
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
|
||||
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polygon", () => {
|
||||
const poly: Polygon<GlobalPoint> = polygon(
|
||||
pointFrom(10, 10),
|
||||
pointFrom(50, 10),
|
||||
pointFrom(50, 50),
|
||||
pointFrom(10, 50),
|
||||
point(10, 10),
|
||||
point(50, 10),
|
||||
point(50, 50),
|
||||
point(10, 50),
|
||||
);
|
||||
|
||||
it("point on polygon", () => {
|
||||
expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true);
|
||||
expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true);
|
||||
expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false);
|
||||
expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false);
|
||||
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
|
||||
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
|
||||
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in polygon", () => {
|
||||
const poly: Polygon<GlobalPoint> = polygon(
|
||||
pointFrom(0, 0),
|
||||
pointFrom(2, 0),
|
||||
pointFrom(2, 2),
|
||||
pointFrom(0, 2),
|
||||
point(0, 0),
|
||||
point(2, 0),
|
||||
point(2, 2),
|
||||
point(0, 2),
|
||||
);
|
||||
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
|
||||
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
|
||||
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
|
||||
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and ellipse", () => {
|
||||
const ellipse: Ellipse<GlobalPoint> = {
|
||||
center: pointFrom(0, 0),
|
||||
center: point(0, 0),
|
||||
angle: 0 as Radians,
|
||||
halfWidth: 2,
|
||||
halfHeight: 1,
|
||||
};
|
||||
|
||||
it("point on ellipse", () => {
|
||||
[
|
||||
pointFrom(0, 1),
|
||||
pointFrom(0, -1),
|
||||
pointFrom(2, 0),
|
||||
pointFrom(-2, 0),
|
||||
].forEach((p) => {
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
expect(pointOnEllipse(pointFrom(-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.7), ellipse, 0.1)).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(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse(pointFrom(1, -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.1)).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(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in ellipse", () => {
|
||||
[
|
||||
pointFrom(0, 1),
|
||||
pointFrom(0, -1),
|
||||
pointFrom(2, 0),
|
||||
pointFrom(-2, 0),
|
||||
].forEach((p) => {
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
|
||||
expect(pointInEllipse(pointFrom(-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(point(1, -0.8), ellipse)).toBe(true);
|
||||
|
||||
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line and line", () => {
|
||||
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 4),
|
||||
pointFrom(3, 4),
|
||||
);
|
||||
const lineB: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(2, 1),
|
||||
pointFrom(2, 7),
|
||||
);
|
||||
const lineC: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 8),
|
||||
pointFrom(3, 8),
|
||||
);
|
||||
const lineD: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 8),
|
||||
pointFrom(3, 8),
|
||||
);
|
||||
const lineE: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 9),
|
||||
pointFrom(3, 9),
|
||||
);
|
||||
const lineF: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(1, 2),
|
||||
pointFrom(3, 4),
|
||||
);
|
||||
const lineG: LineSegment<GlobalPoint> = lineSegment(
|
||||
pointFrom(0, 1),
|
||||
pointFrom(2, 3),
|
||||
);
|
||||
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
||||
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
||||
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
||||
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
||||
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
||||
|
||||
it("intersection", () => {
|
||||
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