Compare commits

..

2 Commits

Author SHA1 Message Date
Mark Tolmacs 521896cccf Fix lint 2024-09-23 12:47:04 +02:00
Mark Tolmacs fe318126bd Warn to save content
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2024-09-23 12:12:58 +02:00
110 changed files with 1153 additions and 5236 deletions
+7 -3
View File
@@ -4,7 +4,6 @@ import { trackEvent } from "../packages/excalidraw/analytics";
import { getDefaultAppState } from "../packages/excalidraw/appState"; import { getDefaultAppState } from "../packages/excalidraw/appState";
import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog"; import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
import { TopErrorBoundary } from "./components/TopErrorBoundary"; import { TopErrorBoundary } from "./components/TopErrorBoundary";
import { useMathSubtype } from "../packages/excalidraw/element/subtypes/mathjax";
import { import {
APP_NAME, APP_NAME,
EVENT, EVENT,
@@ -127,6 +126,8 @@ import DebugCanvas, {
loadSavedDebugState, loadSavedDebugState,
} from "./components/DebugCanvas"; } from "./components/DebugCanvas";
import { AIComponents } from "./components/AI"; import { AIComponents } from "./components/AI";
import type { SaveWarningRef } from "./components/SaveWarning";
import { SaveWarning } from "./components/SaveWarning";
polyfill(); polyfill();
@@ -332,6 +333,8 @@ const ExcalidrawWrapper = () => {
const [langCode, setLangCode] = useAppLangCode(); const [langCode, setLangCode] = useAppLangCode();
const activityRef = useRef<SaveWarningRef | null>(null);
// initial state // initial state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -356,8 +359,6 @@ const ExcalidrawWrapper = () => {
const [excalidrawAPI, excalidrawRefCallback] = const [excalidrawAPI, excalidrawRefCallback] =
useCallbackRefState<ExcalidrawImperativeAPI>(); useCallbackRefState<ExcalidrawImperativeAPI>();
useMathSubtype(excalidrawAPI);
const [, setShareDialogState] = useAtom(shareDialogStateAtom); const [, setShareDialogState] = useAtom(shareDialogStateAtom);
const [collabAPI] = useAtom(collabAPIAtom); const [collabAPI] = useAtom(collabAPIAtom);
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
@@ -618,6 +619,8 @@ const ExcalidrawWrapper = () => {
collabAPI.syncElements(elements); collabAPI.syncElements(elements);
} }
activityRef.current?.activity();
// this check is redundant, but since this is a hot path, it's best // this check is redundant, but since this is a hot path, it's best
// not to evaludate the nested expression every time // not to evaludate the nested expression every time
if (!LocalData.isSavePaused()) { if (!LocalData.isSavePaused()) {
@@ -859,6 +862,7 @@ const ExcalidrawWrapper = () => {
setTheme={(theme) => setAppTheme(theme)} setTheme={(theme) => setAppTheme(theme)}
refresh={() => forceRefresh((prev) => !prev)} refresh={() => forceRefresh((prev) => !prev)}
/> />
<SaveWarning ref={activityRef} />
<AppWelcomeScreen <AppWelcomeScreen
onCollabDialogOpen={onCollabDialogOpen} onCollabDialogOpen={onCollabDialogOpen}
isCollabEnabled={!isCollabDisabled} isCollabEnabled={!isCollabDisabled}
+40
View File
@@ -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>
);
});
+9
View File
@@ -130,6 +130,15 @@
</script> </script>
<% } %> <% } %>
<!-- For Nunito only preload the latin range, which should be good enough for now -->
<link
rel="preload"
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- Register Assistant as the UI font, before the scene inits --> <!-- Register Assistant as the UI font, before the scene inits -->
<link <link
rel="stylesheet" rel="stylesheet"
+37
View File
@@ -18,6 +18,43 @@
margin-inline-start: auto; margin-inline-start: auto;
} }
.alert-save {
position: absolute;
z-index: 10;
left: 0;
right: 0;
bottom: 10vh;
margin-inline: auto;
width: fit-content;
opacity: 0;
transition: all 0s;
&.animate {
opacity: 1;
transition: all 0.2s ease-in;
}
.dialog {
margin-inline: 10px;
padding: 1rem;
padding-inline: 1.25rem;
resize: none;
white-space: pre-wrap;
box-sizing: border-box;
background-color: var(--color-warning);
border-radius: var(--border-radius-md);
border: 1px solid var(--dialog-border-color);
font-size: 0.875rem;
text-align: center;
line-height: 1.5;
color: var(--color-text-warning);
}
}
.encrypted-icon { .encrypted-icon {
border-radius: var(--space-factor); border-radius: var(--space-factor);
color: var(--color-primary); color: var(--color-primary);
-2
View File
@@ -48,8 +48,6 @@ export default defineConfig({
}, },
}, },
sourcemap: true, sourcemap: true,
// don't auto-inline small assets (i.e. fonts hosted on CDN)
assetsInlineLimit: 0,
}, },
plugins: [ plugins: [
woff2BrowserPlugin(), woff2BrowserPlugin(),
-3
View File
@@ -32,9 +32,7 @@
"husky": "7.0.4", "husky": "7.0.4",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"patch-package": "8.0.0",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"postinstall-postinstall": "2.1.0",
"prettier": "2.6.2", "prettier": "2.6.2",
"rewire": "6.0.0", "rewire": "6.0.0",
"typescript": "4.9.4", "typescript": "4.9.4",
@@ -63,7 +61,6 @@
"locales-coverage": "node scripts/build-locales-coverage.js", "locales-coverage": "node scripts/build-locales-coverage.js",
"locales-coverage:description": "node scripts/locales-coverage-description.js", "locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "patch-package",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "yarn --cwd ./excalidraw-app start", "start": "yarn --cwd ./excalidraw-app start",
"start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o", "start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
-3
View File
@@ -15,8 +15,6 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517)
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135) - `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348) - Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
@@ -305,7 +303,6 @@ define: {
## 0.16.0 (2023-09-19) ## 0.16.0 (2023-09-19)
- Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037).
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546) - Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691) - Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691) - Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
@@ -10,7 +10,7 @@ import {
computeBoundTextPosition, computeBoundTextPosition,
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
getBoundTextElement, getBoundTextElement,
measureTextElement, measureText,
redrawTextBoundingBox, redrawTextBoundingBox,
} from "../element/textElement"; } from "../element/textElement";
import { import {
@@ -31,7 +31,7 @@ import type {
} from "../element/types"; } from "../element/types";
import type { AppState } from "../types"; import type { AppState } from "../types";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
import { arrayToMap } from "../utils"; import { arrayToMap, getFontString } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex"; import { syncMovedIndices } from "../fractionalIndex";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -51,9 +51,11 @@ export const actionUnbindText = register({
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) { if (boundTextElement) {
const { width, height } = measureTextElement(boundTextElement, { const { width, height } = measureText(
text: boundTextElement.originalText, boundTextElement.originalText,
}); getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const originalContainerHeight = getOriginalContainerHeightFromCache( const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id, element.id,
); );
@@ -15,7 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
import type { AppState } from "../types"; import type { AppState } from "../types";
import { resetCursor } from "../cursor"; import { resetCursor } from "../cursor";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { pointFrom } from "../../math"; import { point } from "../../math";
import { isPathALoop } from "../shapes"; import { isPathALoop } from "../shapes";
export const actionFinalize = register({ export const actionFinalize = register({
@@ -115,7 +115,7 @@ export const actionFinalize = register({
mutateElement(multiPointElement, { mutateElement(multiPointElement, {
points: linePoints.map((p, index) => points: linePoints.map((p, index) =>
index === linePoints.length - 1 index === linePoints.length - 1
? pointFrom(firstPoint[0], firstPoint[1]) ? point(firstPoint[0], firstPoint[1])
: p, : p,
), ),
}); });
@@ -2,7 +2,7 @@ import React from "react";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { render } from "../tests/test-utils"; import { render } from "../tests/test-utils";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { pointFrom } from "../../math"; import { point } from "../../math";
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip"; import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
const { h } = window; const { h } = window;
@@ -50,11 +50,11 @@ describe("flipping re-centers selection", () => {
startArrowhead: null, startArrowhead: null,
endArrowhead: "arrow", endArrowhead: "arrow",
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(0, -35), point(0, -35),
pointFrom(-90.9, -35), point(-90.9, -35),
pointFrom(-90.9, 204.9), point(-90.9, 204.9),
pointFrom(65.1, 204.9), point(65.1, 204.9),
], ],
elbowed: true, elbowed: true,
}), }),
@@ -116,7 +116,7 @@ import {
import { mutateElbowArrow } from "../element/routing"; import { mutateElbowArrow } from "../element/routing";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom, vector } from "../../math"; import { point, vector } from "../../math";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@@ -1651,7 +1651,7 @@ export const actionChangeArrowType = register({
elementsMap, elementsMap,
[finalStartPoint, finalEndPoint].map( [finalStartPoint, finalEndPoint].map(
(p): LocalPoint => (p): LocalPoint =>
pointFrom(p[0] - newElement.x, p[1] - newElement.y), point(p[0] - newElement.x, p[1] - newElement.y),
), ),
vector(0, 0), vector(0, 0),
{ {
+8 -60
View File
@@ -6,7 +6,6 @@ import type {
ActionResult, ActionResult,
PanelComponentProps, PanelComponentProps,
ActionSource, ActionSource,
ActionPredicateFn,
} from "./types"; } from "./types";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
@@ -46,7 +45,6 @@ const trackAction = (
export class ActionManager { export class ActionManager {
actions = {} as Record<ActionName, Action>; actions = {} as Record<ActionName, Action>;
actionPredicates = [] as ActionPredicateFn[];
updater: (actionResult: ActionResult | Promise<ActionResult>) => void; updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
@@ -74,37 +72,6 @@ export class ActionManager {
this.app = app; this.app = app;
} }
registerActionPredicate(predicate: ActionPredicateFn) {
if (!this.actionPredicates.includes(predicate)) {
this.actionPredicates.push(predicate);
}
}
filterActions(
filter: ActionPredicateFn,
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
},
): Action[] {
// For testing
if (this === undefined) {
return [];
}
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
const appState = this.getAppState();
const data = opts?.data;
const actions: Action[] = [];
for (const key in this.actions) {
const action = this.actions[key as ActionName];
if (filter(action, elements, appState, this.app, data)) {
actions.push(action);
}
}
return actions;
}
registerAction(action: Action) { registerAction(action: Action) {
this.actions[action.name] = action; this.actions[action.name] = action;
} }
@@ -121,7 +88,7 @@ export class ActionManager {
(action) => (action) =>
(action.name in canvasActions (action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions] ? canvasActions[action.name as keyof typeof canvasActions]
: this.isActionEnabled(action, { noPredicates: true })) && : true) &&
action.keyTest && action.keyTest &&
action.keyTest( action.keyTest(
event, event,
@@ -180,7 +147,7 @@ export class ActionManager {
"PanelComponent" in this.actions[name] && "PanelComponent" in this.actions[name] &&
(name in canvasActions (name in canvasActions
? canvasActions[name as keyof typeof canvasActions] ? canvasActions[name as keyof typeof canvasActions]
: this.isActionEnabled(this.actions[name], { noPredicates: true })) : true)
) { ) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
@@ -202,7 +169,6 @@ export class ActionManager {
return ( return (
<PanelComponent <PanelComponent
key={name}
elements={this.getElementsIncludingDeleted()} elements={this.getElementsIncludingDeleted()}
appState={this.getAppState()} appState={this.getAppState()}
updateData={updateData} updateData={updateData}
@@ -216,31 +182,13 @@ export class ActionManager {
return null; return null;
}; };
isActionEnabled = ( isActionEnabled = (action: Action) => {
action: Action, const elements = this.getElementsIncludingDeleted();
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
noPredicates?: boolean;
},
): boolean => {
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
const appState = this.getAppState(); const appState = this.getAppState();
const data = opts?.data;
if ( return (
!opts?.noPredicates && !action.predicate ||
action.predicate && action.predicate(elements, appState, this.app.props, this.app)
!action.predicate(elements, appState, this.app.props, this.app, data) );
) {
return false;
}
let enabled = true;
this.actionPredicates.forEach((fn) => {
if (!fn(action, elements, appState, this.app, data)) {
enabled = false;
}
});
return enabled;
}; };
} }
+1 -11
View File
@@ -2,12 +2,11 @@ import { isDarwin } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import type { SubtypeOf } from "../utility-types"; import type { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import type { ActionName, CustomActionName } from "./types"; import type { ActionName } from "./types";
export type ShortcutName = export type ShortcutName =
| SubtypeOf< | SubtypeOf<
ActionName, ActionName,
| CustomActionName
| "toggleTheme" | "toggleTheme"
| "loadScene" | "loadScene"
| "clearCanvas" | "clearCanvas"
@@ -55,15 +54,6 @@ export type ShortcutName =
| "commandPalette" | "commandPalette"
| "searchMenu"; | "searchMenu";
export const registerCustomShortcuts = (
shortcuts: Record<CustomActionName, string[]>,
) => {
for (const key in shortcuts) {
const shortcut = key as CustomActionName;
shortcutMap[shortcut] = shortcuts[shortcut];
}
};
const shortcutMap: Record<ShortcutName, string[]> = { const shortcutMap: Record<ShortcutName, string[]> = {
toggleTheme: [getShortcutKey("Shift+Alt+D")], toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")], saveScene: [getShortcutKey("CtrlOrCmd+S")],
-15
View File
@@ -41,24 +41,10 @@ type ActionFn = (
app: AppClassProperties, app: AppClassProperties,
) => ActionResult | Promise<ActionResult>; ) => ActionResult | Promise<ActionResult>;
// Return `true` *unless* `Action` should be disabled
// given `elements`, `appState`, and optionally `data`.
export type ActionPredicateFn = (
action: Action,
elements: readonly ExcalidrawElement[],
appState: AppState,
app: AppClassProperties,
data?: Record<string, any>,
) => boolean;
export type UpdaterFn = (res: ActionResult) => void; export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void; export type ActionFilterFn = (action: Action) => void;
export const makeCustomActionName = (name: string) =>
`custom.${name}` as CustomActionName;
export type CustomActionName = `custom.${string}`;
export type ActionName = export type ActionName =
| CustomActionName
| "copy" | "copy"
| "cut" | "cut"
| "paste" | "paste"
@@ -193,7 +179,6 @@ export interface Action {
appState: AppState, appState: AppState,
appProps: ExcalidrawProps, appProps: ExcalidrawProps,
app: AppClassProperties, app: AppClassProperties,
data?: Record<string, any>,
) => boolean; ) => boolean;
checked?: (appState: Readonly<AppState>) => boolean; checked?: (appState: Readonly<AppState>) => boolean;
trackEvent: trackEvent:
-2
View File
@@ -170,8 +170,6 @@ const APP_STATE_STORAGE_CONF = (<
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false }, editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false }, activeTool: { browser: true, export: false, server: false },
activeSubtypes: { browser: true, export: false, server: false },
customData: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false }, penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false }, penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
+6 -26
View File
@@ -1,5 +1,5 @@
import type { Radians } from "../math"; import type { Radians } from "../math";
import { pointFrom } from "../math"; import { point } from "../math";
import { import {
COLOR_PALETTE, COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX, DEFAULT_CHART_COLOR_INDEX,
@@ -13,8 +13,6 @@ import {
import { newElement, newLinearElement, newTextElement } from "./element"; import { newElement, newLinearElement, newTextElement } from "./element";
import type { NonDeletedExcalidrawElement } from "./element/types"; import type { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
import type { AppState } from "./types";
import { selectSubtype } from "./element/subtypes";
export type ChartElements = readonly NonDeletedExcalidrawElement[]; export type ChartElements = readonly NonDeletedExcalidrawElement[];
@@ -27,8 +25,6 @@ export interface Spreadsheet {
title: string | null; title: string | null;
labels: string[] | null; labels: string[] | null;
values: number[]; values: number[];
activeSubtypes?: AppState["activeSubtypes"];
customData?: AppState["customData"];
} }
export const NOT_SPREADSHEET = "NOT_SPREADSHEET"; export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
@@ -199,17 +195,13 @@ const chartXLabels = (
groupId: string, groupId: string,
backgroundColor: string, backgroundColor: string,
): ChartElements => { ): ChartElements => {
const custom = selectSubtype(spreadsheet, "text");
return ( return (
spreadsheet.labels?.map((label, index) => { spreadsheet.labels?.map((label, index) => {
return newTextElement({ return newTextElement({
groupIds: [groupId], groupIds: [groupId],
backgroundColor, backgroundColor,
...commonProps, ...commonProps,
text: text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
label.length > 8 && custom.subtype === undefined
? `${label.slice(0, 5)}...`
: label,
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2, x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
y: y + BAR_GAP / 2, y: y + BAR_GAP / 2,
width: BAR_WIDTH, width: BAR_WIDTH,
@@ -217,7 +209,6 @@ const chartXLabels = (
fontSize: 16, fontSize: 16,
textAlign: "center", textAlign: "center",
verticalAlign: "top", verticalAlign: "top",
...custom,
}); });
}) || [] }) || []
); );
@@ -238,7 +229,6 @@ const chartYLabels = (
y: y - BAR_GAP, y: y - BAR_GAP,
text: "0", text: "0",
textAlign: "right", textAlign: "right",
...selectSubtype(spreadsheet, "text"),
}); });
const maxYLabel = newTextElement({ const maxYLabel = newTextElement({
@@ -249,7 +239,6 @@ const chartYLabels = (
y: y - BAR_HEIGHT - minYLabel.height / 2, y: y - BAR_HEIGHT - minYLabel.height / 2,
text: Math.max(...spreadsheet.values).toLocaleString(), text: Math.max(...spreadsheet.values).toLocaleString(),
textAlign: "right", textAlign: "right",
...selectSubtype(spreadsheet, "text"),
}); });
return [minYLabel, maxYLabel]; return [minYLabel, maxYLabel];
@@ -271,8 +260,7 @@ const chartLines = (
x, x,
y, y,
width: chartWidth, width: chartWidth,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)], points: [point(0, 0), point(chartWidth, 0)],
...selectSubtype(spreadsheet, "line"),
}); });
const yLine = newLinearElement({ const yLine = newLinearElement({
@@ -283,8 +271,7 @@ const chartLines = (
x, x,
y, y,
height: chartHeight, height: chartHeight,
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)], points: [point(0, 0), point(0, -chartHeight)],
...selectSubtype(spreadsheet, "line"),
}); });
const maxLine = newLinearElement({ const maxLine = newLinearElement({
@@ -297,8 +284,7 @@ const chartLines = (
strokeStyle: "dotted", strokeStyle: "dotted",
width: chartWidth, width: chartWidth,
opacity: GRID_OPACITY, opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)], points: [point(0, 0), point(chartWidth, 0)],
...selectSubtype(spreadsheet, "line"),
}); });
return [xLine, yLine, maxLine]; return [xLine, yLine, maxLine];
@@ -325,7 +311,6 @@ const chartBaseElements = (
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE, y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null, roundness: null,
textAlign: "center", textAlign: "center",
...selectSubtype(spreadsheet, "text"),
}) })
: null; : null;
@@ -342,7 +327,6 @@ const chartBaseElements = (
strokeColor: COLOR_PALETTE.black, strokeColor: COLOR_PALETTE.black,
fillStyle: "solid", fillStyle: "solid",
opacity: 6, opacity: 6,
...selectSubtype(spreadsheet, "rectangle"),
}) })
: null; : null;
@@ -375,7 +359,6 @@ const chartTypeBar = (
y: y - barHeight - BAR_GAP, y: y - barHeight - BAR_GAP,
width: BAR_WIDTH, width: BAR_WIDTH,
height: barHeight, height: barHeight,
...selectSubtype(spreadsheet, "rectangle"),
}); });
}); });
@@ -426,7 +409,6 @@ const chartTypeLine = (
width: maxX - minX, width: maxX - minX,
strokeWidth: 2, strokeWidth: 2,
points: points as any, points: points as any,
...selectSubtype(spreadsheet, "line"),
}); });
const dots = spreadsheet.values.map((value, index) => { const dots = spreadsheet.values.map((value, index) => {
@@ -443,7 +425,6 @@ const chartTypeLine = (
y: y + cy - BAR_GAP * 2, y: y + cy - BAR_GAP * 2,
width: BAR_GAP, width: BAR_GAP,
height: BAR_GAP, height: BAR_GAP,
...selectSubtype(spreadsheet, "ellipse"),
}); });
}); });
@@ -460,8 +441,7 @@ const chartTypeLine = (
height: cy, height: cy,
strokeStyle: "dotted", strokeStyle: "dotted",
opacity: GRID_OPACITY, opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(0, cy)], points: [point(0, 0), point(0, cy)],
...selectSubtype(spreadsheet, "line"),
}); });
}); });
+1 -6
View File
@@ -2,7 +2,7 @@ import type {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import type { AppState, BinaryFiles } from "./types"; import type { BinaryFiles } from "./types";
import type { Spreadsheet } from "./charts"; import type { Spreadsheet } from "./charts";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts"; import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import { import {
@@ -333,7 +333,6 @@ const parseClipboardEvent = async (
export const parseClipboard = async ( export const parseClipboard = async (
event: ClipboardEvent, event: ClipboardEvent,
isPlainPaste = false, isPlainPaste = false,
appState?: AppState,
): Promise<ClipboardData> => { ): Promise<ClipboardData> => {
const parsedEventData = await parseClipboardEvent(event, isPlainPaste); const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
@@ -350,10 +349,6 @@ export const parseClipboard = async (
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value); !isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
if (spreadsheetResult) { if (spreadsheetResult) {
if ("spreadsheet" in spreadsheetResult) {
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
spreadsheetResult.spreadsheet.customData = appState?.customData;
}
return spreadsheetResult; return spreadsheetResult;
} }
} catch (error: any) { } catch (error: any) {
@@ -21,7 +21,6 @@ import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils"; import { capitalizeString, isTransparent } from "../utils";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { SubtypeShapeActions } from "./Subtypes";
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons"; import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { import {
@@ -137,7 +136,6 @@ export const SelectedShapeActions = ({
{canChangeBackgroundColor(appState, targetElements) && ( {canChangeBackgroundColor(appState, targetElements) && (
<div>{renderAction("changeBackgroundColor")}</div> <div>{renderAction("changeBackgroundColor")}</div>
)} )}
<SubtypeShapeActions elements={targetElements} />
{showFillIcons && renderAction("changeFillStyle")} {showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) || {(hasStrokeWidth(appState.activeTool.type) ||
+30 -124
View File
@@ -301,18 +301,6 @@ import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
import LayerUI from "./LayerUI"; import LayerUI from "./LayerUI";
import { Toast } from "./Toast"; import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import type {
SubtypeLoadedCb,
SubtypeRecord,
SubtypePrepFn,
} from "../element/subtypes";
import {
checkRefreshOnSubtypeLoad,
isSubtypeAction,
prepareSubtype,
selectSubtype,
subtypeActionPredicate,
} from "../element/subtypes";
import { import {
dataURLToFile, dataURLToFile,
generateIdFromFile, generateIdFromFile,
@@ -457,7 +445,7 @@ import {
} from "../element/flowchart"; } from "../element/flowchart";
import { searchItemInFocusAtom } from "./SearchMenu"; import { searchItemInFocusAtom } from "./SearchMenu";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { pointFrom, pointDistance, vector } from "../../math"; import { point, pointDistance, vector } from "../../math";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -722,7 +710,6 @@ class App extends React.Component<AppProps, AppState> {
registerAction: (action: Action) => { registerAction: (action: Action) => {
this.actionManager.registerAction(action); this.actionManager.registerAction(action);
}, },
addSubtype: this.addSubtype,
refresh: this.refresh, refresh: this.refresh,
setToast: this.setToast, setToast: this.setToast,
id: this.id, id: this.id,
@@ -759,19 +746,6 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.registerAction( this.actionManager.registerAction(
createRedoAction(this.history, this.store), createRedoAction(this.history, this.store),
); );
this.actionManager.registerActionPredicate(subtypeActionPredicate);
}
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
const elements = this.getSceneElementsIncludingDeleted();
// If there are any elements of the just-registered subtype,
// refresh the scene to re-render each such element.
if (checkRefreshOnSubtypeLoad(hasSubtype, elements)) {
this.refresh();
}
};
return prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
} }
private onWindowMessage(event: MessageEvent) { private onWindowMessage(event: MessageEvent) {
@@ -2977,7 +2951,7 @@ class App extends React.Component<AppProps, AppState> {
// event else some browsers (FF...) will clear the clipboardData // event else some browsers (FF...) will clear the clipboardData
// (something something security) // (something something security)
let file = event?.clipboardData?.files[0]; let file = event?.clipboardData?.files[0];
const data = await parseClipboard(event, isPlainPaste, this.state); const data = await parseClipboard(event, isPlainPaste);
if (!file && !isPlainPaste) { if (!file && !isPlainPaste) {
if (data.mixedContent) { if (data.mixedContent) {
return this.addElementsFromMixedContentPaste(data.mixedContent, { return this.addElementsFromMixedContentPaste(data.mixedContent, {
@@ -3415,7 +3389,6 @@ class App extends React.Component<AppProps, AppState> {
fontFamily: this.state.currentItemFontFamily, fontFamily: this.state.currentItemFontFamily,
textAlign: DEFAULT_TEXT_ALIGN, textAlign: DEFAULT_TEXT_ALIGN,
verticalAlign: DEFAULT_VERTICAL_ALIGN, verticalAlign: DEFAULT_VERTICAL_ALIGN,
...selectSubtype(this.state, "text"),
locked: false, locked: false,
}; };
const fontString = getFontString({ const fontString = getFontString({
@@ -4937,7 +4910,7 @@ class App extends React.Component<AppProps, AppState> {
this.getElementHitThreshold(), this.getElementHitThreshold(),
); );
return isPointInShape(pointFrom(x, y), selectionShape); return isPointInShape(point(x, y), selectionShape);
} }
// take bound text element into consideration for hit collision as well // take bound text element into consideration for hit collision as well
@@ -5125,7 +5098,6 @@ class App extends React.Component<AppProps, AppState> {
verticalAlign: parentCenterPosition verticalAlign: parentCenterPosition
? VERTICAL_ALIGN.MIDDLE ? VERTICAL_ALIGN.MIDDLE
: DEFAULT_VERTICAL_ALIGN, : DEFAULT_VERTICAL_ALIGN,
...selectSubtype(this.state, "text"),
containerId: shouldBindToContainer ? container?.id : undefined, containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [], groupIds: container?.groupIds ?? [],
lineHeight, lineHeight,
@@ -5297,7 +5269,7 @@ class App extends React.Component<AppProps, AppState> {
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
pointFrom(scenePointer.x, scenePointer.y), point(scenePointer.x, scenePointer.y),
this.device.editor.isMobile, this.device.editor.isMobile,
) )
); );
@@ -5309,14 +5281,11 @@ class App extends React.Component<AppProps, AppState> {
isTouchScreen: boolean, isTouchScreen: boolean,
) => { ) => {
const draggedDistance = pointDistance( const draggedDistance = pointDistance(
pointFrom( point(
this.lastPointerDownEvent!.clientX, this.lastPointerDownEvent!.clientX,
this.lastPointerDownEvent!.clientY, this.lastPointerDownEvent!.clientY,
), ),
pointFrom( point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY),
this.lastPointerUpEvent!.clientX,
this.lastPointerUpEvent!.clientY,
),
); );
if ( if (
!this.hitLinkElement || !this.hitLinkElement ||
@@ -5335,7 +5304,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
elementsMap, elementsMap,
this.state, this.state,
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y), point(lastPointerDownCoords.x, lastPointerDownCoords.y),
this.device.editor.isMobile, this.device.editor.isMobile,
); );
const lastPointerUpCoords = viewportCoordsToSceneCoords( const lastPointerUpCoords = viewportCoordsToSceneCoords(
@@ -5346,7 +5315,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
elementsMap, elementsMap,
this.state, this.state,
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y), point(lastPointerUpCoords.x, lastPointerUpCoords.y),
this.device.editor.isMobile, this.device.editor.isMobile,
); );
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
@@ -5596,7 +5565,7 @@ class App extends React.Component<AppProps, AppState> {
// threshold, add a point // threshold, add a point
if ( if (
pointDistance( pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry), point(scenePointerX - rx, scenePointerY - ry),
lastPoint, lastPoint,
) >= LINE_CONFIRM_THRESHOLD ) >= LINE_CONFIRM_THRESHOLD
) { ) {
@@ -5605,7 +5574,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
points: [ points: [
...points, ...points,
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry), point<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
], ],
}, },
false, false,
@@ -5619,7 +5588,7 @@ class App extends React.Component<AppProps, AppState> {
points.length > 2 && points.length > 2 &&
lastCommittedPoint && lastCommittedPoint &&
pointDistance( pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry), point(scenePointerX - rx, scenePointerY - ry),
lastCommittedPoint, lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD ) < LINE_CONFIRM_THRESHOLD
) { ) {
@@ -5667,7 +5636,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
[ [
...points.slice(0, -1), ...points.slice(0, -1),
pointFrom<LocalPoint>( point<LocalPoint>(
lastCommittedX + dxFromLastCommitted, lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted, lastCommittedY + dyFromLastCommitted,
), ),
@@ -5686,7 +5655,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
points: [ points: [
...points.slice(0, -1), ...points.slice(0, -1),
pointFrom<LocalPoint>( point<LocalPoint>(
lastCommittedX + dxFromLastCommitted, lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted, lastCommittedY + dyFromLastCommitted,
), ),
@@ -5915,8 +5884,8 @@ class App extends React.Component<AppProps, AppState> {
}; };
const distance = pointDistance( const distance = pointDistance(
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y), point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
pointFrom(scenePointer.x, scenePointer.y), point(scenePointer.x, scenePointer.y),
); );
const threshold = this.getElementHitThreshold(); const threshold = this.getElementHitThreshold();
const p = { ...pointerDownState.lastCoords }; const p = { ...pointerDownState.lastCoords };
@@ -6428,7 +6397,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
pointFrom(scenePointer.x, scenePointer.y), point(scenePointer.x, scenePointer.y),
) )
) { ) {
this.handleEmbeddableCenterClick(this.hitLinkElement); this.handleEmbeddableCenterClick(this.hitLinkElement);
@@ -7119,7 +7088,7 @@ class App extends React.Component<AppProps, AppState> {
simulatePressure, simulatePressure,
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
points: [pointFrom<LocalPoint>(0, 0)], points: [point<LocalPoint>(0, 0)],
pressures: simulatePressure ? [] : [event.pressure], pressures: simulatePressure ? [] : [event.pressure],
}); });
@@ -7282,7 +7251,6 @@ class App extends React.Component<AppProps, AppState> {
roughness: this.state.currentItemRoughness, roughness: this.state.currentItemRoughness,
roundness: null, roundness: null,
opacity: this.state.currentItemOpacity, opacity: this.state.currentItemOpacity,
...selectSubtype(this.state, "image"),
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
}); });
@@ -7329,10 +7297,7 @@ class App extends React.Component<AppProps, AppState> {
multiElement.points.length > 1 && multiElement.points.length > 1 &&
lastCommittedPoint && lastCommittedPoint &&
pointDistance( pointDistance(
pointFrom( point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry),
pointerDownState.origin.x - rx,
pointerDownState.origin.y - ry,
),
lastCommittedPoint, lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD ) < LINE_CONFIRM_THRESHOLD
) { ) {
@@ -7399,7 +7364,6 @@ class App extends React.Component<AppProps, AppState> {
null, null,
startArrowhead, startArrowhead,
endArrowhead, endArrowhead,
...selectSubtype(this.state, elementType),
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
@@ -7419,7 +7383,6 @@ class App extends React.Component<AppProps, AppState> {
this.state.currentItemRoundness === "round" this.state.currentItemRoundness === "round"
? { type: ROUNDNESS.PROPORTIONAL_RADIUS } ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
: null, : null,
...selectSubtype(this.state, elementType),
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
}); });
@@ -7436,7 +7399,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
}); });
mutateElement(element, { mutateElement(element, {
points: [...element.points, pointFrom<LocalPoint>(0, 0)], points: [...element.points, point<LocalPoint>(0, 0)],
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
@@ -7500,7 +7463,6 @@ class App extends React.Component<AppProps, AppState> {
roughness: this.state.currentItemRoughness, roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity, opacity: this.state.currentItemOpacity,
roundness: this.getCurrentItemRoundness(elementType), roundness: this.getCurrentItemRoundness(elementType),
...selectSubtype(this.state, elementType),
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
} as const; } as const;
@@ -7690,8 +7652,8 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if ( if (
pointDistance( pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y), point(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y), point(pointerDownState.origin.x, pointerDownState.origin.y),
) < DRAGGING_THRESHOLD ) < DRAGGING_THRESHOLD
) { ) {
return; return;
@@ -8040,7 +8002,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, point<LocalPoint>(dx, dy)],
pressures, pressures,
}, },
false, false,
@@ -8069,7 +8031,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, point<LocalPoint>(dx, dy)],
}, },
false, false,
); );
@@ -8077,7 +8039,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElbowArrow( mutateElbowArrow(
newElement, newElement,
elementsMap, elementsMap,
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)], [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
vector(0, 0), vector(0, 0),
undefined, undefined,
{ {
@@ -8089,7 +8051,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)], points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
}, },
false, false,
); );
@@ -8398,9 +8360,9 @@ class App extends React.Component<AppProps, AppState> {
: [...newElement.pressures, childEvent.pressure]; : [...newElement.pressures, childEvent.pressure];
mutateElement(newElement, { mutateElement(newElement, {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [...points, point<LocalPoint>(dx, dy)],
pressures, pressures,
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy), lastCommittedPoint: point<LocalPoint>(dx, dy),
}); });
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
@@ -8447,7 +8409,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement(newElement, { mutateElement(newElement, {
points: [ points: [
...newElement.points, ...newElement.points,
pointFrom<LocalPoint>( point<LocalPoint>(
pointerCoords.x - newElement.x, pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y, pointerCoords.y - newElement.y,
), ),
@@ -8761,8 +8723,8 @@ class App extends React.Component<AppProps, AppState> {
this.eraserTrail.endPath(); this.eraserTrail.endPath();
const draggedDistance = pointDistance( const draggedDistance = pointDistance(
pointFrom(pointerStart.clientX, pointerStart.clientY), point(pointerStart.clientX, pointerStart.clientY),
pointFrom(pointerEnd.clientX, pointerEnd.clientY), point(pointerEnd.clientX, pointerEnd.clientY),
); );
if (draggedDistance === 0) { if (draggedDistance === 0) {
@@ -10080,39 +10042,6 @@ class App extends React.Component<AppProps, AppState> {
const elementsToHighlight = new Set<ExcalidrawElement>(); const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => { selectedFrames.forEach((frame) => {
const elementsInFrame = getFrameChildren(
this.scene.getNonDeletedElements(),
frame.id,
);
// keep elements' positions relative to their frames on frames resizing
if (transformHandleType) {
if (transformHandleType.includes("w")) {
elementsInFrame.forEach((element) => {
mutateElement(element, {
x:
frame.x +
(frameElementsOffsetsMap.get(frame.id + element.id)?.x || 0),
y:
frame.y +
(frameElementsOffsetsMap.get(frame.id + element.id)?.y || 0),
});
});
}
if (transformHandleType.includes("n")) {
elementsInFrame.forEach((element) => {
mutateElement(element, {
x:
frame.x +
(frameElementsOffsetsMap.get(frame.id + element.id)?.x || 0),
y:
frame.y +
(frameElementsOffsetsMap.get(frame.id + element.id)?.y || 0),
});
});
}
}
getElementsInResizingFrame( getElementsInResizingFrame(
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
frame, frame,
@@ -10133,29 +10062,6 @@ class App extends React.Component<AppProps, AppState> {
private getContextMenuItems = ( private getContextMenuItems = (
type: "canvas" | "element", type: "canvas" | "element",
): ContextMenuItems => {
const subtype: ContextMenuItems = [];
this.actionManager
.filterActions(isSubtypeAction)
.forEach(
(action) =>
this.actionManager.isActionEnabled(action, { data: {} }) &&
subtype.push(action),
);
if (subtype.length > 0) {
subtype.push(CONTEXT_MENU_SEPARATOR);
}
const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
(item) =>
!item ||
item === CONTEXT_MENU_SEPARATOR ||
this.actionManager.isActionEnabled(item, { noPredicates: true }),
);
return [...subtype, ...standard];
};
private _getContextMenuItems = (
type: "canvas" | "element",
): ContextMenuItems => { ): ContextMenuItems => {
const options: ContextMenuItems = []; const options: ContextMenuItems = [];
@@ -56,7 +56,6 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { SubtypeToggles } from "./Subtypes";
import { LaserPointerButton } from "./LaserPointerButton"; import { LaserPointerButton } from "./LaserPointerButton";
import { TTDDialog } from "./TTDDialog/TTDDialog"; import { TTDDialog } from "./TTDDialog/TTDDialog";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
@@ -300,7 +299,6 @@ const LayerUI = ({
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
<SubtypeToggles />
{isCollaborating && ( {isCollaborating && (
<Island <Island
style={{ style={{
@@ -24,7 +24,6 @@ import { PenModeButton } from "./PenModeButton";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../context/tunnels";
import { SubtypeToggles } from "./Subtypes";
type MobileMenuProps = { type MobileMenuProps = {
appState: UIAppState; appState: UIAppState;
@@ -90,7 +89,6 @@ export const MobileMenu = ({
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
<SubtypeToggles />
{renderTopRightUI && renderTopRightUI(true, appState)} {renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
@@ -3,7 +3,7 @@ import React, { useLayoutEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import type { ChartElements, Spreadsheet } from "../charts"; import type { ChartElements, Spreadsheet } from "../charts";
import { renderSpreadsheet } from "../charts"; import { renderSpreadsheet } from "../charts";
import type { ChartType, ElementsMap } from "../element/types"; import type { ChartType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import type { UIAppState } from "../types"; import type { UIAppState } from "../types";
@@ -11,12 +11,6 @@ import { useApp } from "./App";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss"; import "./PasteChartDialog.scss";
import { ensureSubtypesLoaded } from "../element/subtypes";
import { isTextElement } from "../element";
import {
getContainerElement,
redrawTextBoundingBox,
} from "../element/textElement";
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void; type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
@@ -32,64 +26,41 @@ const ChartPreviewBtn = (props: {
); );
useLayoutEffect(() => { useLayoutEffect(() => {
if (!props.spreadsheet) {
return;
}
const elements = renderSpreadsheet(
props.chartType,
props.spreadsheet,
0,
0,
);
setChartElements(elements);
let svg: SVGSVGElement; let svg: SVGSVGElement;
const previewNode = previewRef.current!; const previewNode = previewRef.current!;
(async () => { (async () => {
(async () => { svg = await exportToSvg(
let elements: ChartElements; elements,
await ensureSubtypesLoaded( {
props.spreadsheet?.activeSubtypes ?? [], exportBackground: false,
() => { viewBackgroundColor: oc.white,
if (!props.spreadsheet) { },
return; null, // files
} );
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();
previewNode.appendChild(svg);
elements = renderSpreadsheet( if (props.selected) {
props.chartType, (previewNode.parentNode as HTMLDivElement).focus();
props.spreadsheet, }
0,
0,
);
const elementsMap = new Map() as ElementsMap;
for (const element of elements) {
if (!element.isDeleted) {
elementsMap.set(element.id, element);
}
}
elements.forEach(
(el) =>
isTextElement(el) &&
redrawTextBoundingBox(
el,
getContainerElement(el, elementsMap),
elementsMap,
),
);
setChartElements(elements);
},
).then(async () => {
svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white,
},
null, // files
);
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();
previewNode.appendChild(svg);
if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
});
})();
return () => {
previewNode.replaceChildren();
};
})(); })();
return () => {
previewNode.replaceChildren();
};
}, [props.spreadsheet, props.chartType, props.selected]); }, [props.spreadsheet, props.chartType, props.selected]);
return ( return (
@@ -20,7 +20,7 @@ import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
import { getElementsInAtomicUnit, resizeElement } from "./utils"; import { getElementsInAtomicUnit, resizeElement } from "./utils";
import type { AtomicUnit } from "./utils"; import type { AtomicUnit } from "./utils";
import { MIN_WIDTH_OR_HEIGHT } from "../../constants"; import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
import { pointFrom, type GlobalPoint } from "../../../math"; import { point, type GlobalPoint } from "../../../math";
interface MultiDimensionProps { interface MultiDimensionProps {
property: "width" | "height"; property: "width" | "height";
@@ -182,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
initialHeight, initialHeight,
aspectRatio, aspectRatio,
pointFrom(x1, y1), point(x1, y1),
property, property,
latestElements, latestElements,
originalElements, originalElements,
@@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
initialHeight, initialHeight,
aspectRatio, aspectRatio,
pointFrom(x1, y1), point(x1, y1),
property, property,
latestElements, latestElements,
originalElements, originalElements,
@@ -13,7 +13,7 @@ import { useMemo } from "react";
import { getElementsInAtomicUnit, moveElement } from "./utils"; import { getElementsInAtomicUnit, moveElement } from "./utils";
import type { AtomicUnit } from "./utils"; import type { AtomicUnit } from "./utils";
import type { AppState } from "../../types"; import type { AppState } from "../../types";
import { pointFrom, pointRotateRads } from "../../../math"; import { point, pointRotateRads } from "../../../math";
interface MultiPositionProps { interface MultiPositionProps {
property: "x" | "y"; property: "x" | "y";
@@ -44,8 +44,8 @@ const moveElements = (
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y), point(origElement.x, origElement.y),
pointFrom(cx, cy), point(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -97,8 +97,8 @@ const moveGroupTo = (
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(latestElement.x, latestElement.y), point(latestElement.x, latestElement.y),
pointFrom(cx, cy), point(cx, cy),
latestElement.angle, latestElement.angle,
); );
@@ -171,8 +171,8 @@ const handlePositionChange: DragInputCallbackType<
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y), point(origElement.x, origElement.y),
pointFrom(cx, cy), point(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -241,8 +241,8 @@ const MultiPosition = ({
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2]; const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(el.x, el.y), point(el.x, el.y),
pointFrom(cx, cy), point(cx, cy),
el.angle, el.angle,
); );
@@ -4,7 +4,7 @@ import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue, moveElement } from "./utils"; import { getStepSizedValue, moveElement } from "./utils";
import type Scene from "../../scene/Scene"; import type Scene from "../../scene/Scene";
import type { AppState } from "../../types"; import type { AppState } from "../../types";
import { pointFrom, pointRotateRads } from "../../../math"; import { point, pointRotateRads } from "../../../math";
interface PositionProps { interface PositionProps {
property: "x" | "y"; property: "x" | "y";
@@ -33,8 +33,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y), point(origElement.x, origElement.y),
pointFrom(cx, cy), point(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -93,8 +93,8 @@ const Position = ({
appState, appState,
}: PositionProps) => { }: PositionProps) => {
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(element.x, element.y), point(element.x, element.y),
pointFrom(element.x + element.width / 2, element.y + element.height / 2), point(element.x + element.width / 2, element.y + element.height / 2),
element.angle, element.angle,
); );
const value = const value =
@@ -25,7 +25,7 @@ import { API } from "../../tests/helpers/api";
import { actionGroup } from "../../actions"; import { actionGroup } from "../../actions";
import { isInGroup } from "../../groups"; import { isInGroup } from "../../groups";
import type { Degrees } from "../../../math"; import type { Degrees } from "../../../math";
import { degreesToRadians, pointFrom, pointRotateRads } from "../../../math"; import { degreesToRadians, point, pointRotateRads } from "../../../math";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -264,8 +264,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -283,8 +283,8 @@ describe("stats for a generic element", () => {
testInputProperty(rectangle, "angle", "A", 0, 45); testInputProperty(rectangle, "angle", "A", 0, 45);
let [newTopLeftX, newTopLeftY] = pointRotateRads( let [newTopLeftX, newTopLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -294,8 +294,8 @@ describe("stats for a generic element", () => {
testInputProperty(rectangle, "angle", "A", 45, 66); testInputProperty(rectangle, "angle", "A", 45, 66);
[newTopLeftX, newTopLeftY] = pointRotateRads( [newTopLeftX, newTopLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
expect(newTopLeftX.toString()).not.toEqual(xInput.value); expect(newTopLeftX.toString()).not.toEqual(xInput.value);
@@ -311,8 +311,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
testInputProperty(rectangle, "width", "W", rectangle.width, 400); testInputProperty(rectangle, "width", "W", rectangle.width, 400);
@@ -321,8 +321,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
let [currentTopLeftX, currentTopLeftY] = pointRotateRads( let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4); expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
@@ -334,8 +334,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
[currentTopLeftX, currentTopLeftY] = pointRotateRads( [currentTopLeftX, currentTopLeftY] = pointRotateRads(
pointFrom(rectangle.x, rectangle.y), point(rectangle.x, rectangle.y),
pointFrom(cx, cy), point(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -1,5 +1,5 @@
import type { Radians } from "../../../math"; import type { Radians } from "../../../math";
import { pointFrom, pointRotateRads } from "../../../math"; import { point, pointRotateRads } from "../../../math";
import { import {
bindOrUnbindLinearElements, bindOrUnbindLinearElements,
updateBoundElements, updateBoundElements,
@@ -231,8 +231,8 @@ export const moveElement = (
originalElement.y + originalElement.height / 2, originalElement.y + originalElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(originalElement.x, originalElement.y), point(originalElement.x, originalElement.y),
pointFrom(cx, cy), point(cx, cy),
originalElement.angle, originalElement.angle,
); );
@@ -240,8 +240,8 @@ export const moveElement = (
const changeInY = newTopLeftY - topLeftY; const changeInY = newTopLeftY - topLeftY;
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
pointFrom(newTopLeftX, newTopLeftY), point(newTopLeftX, newTopLeftY),
pointFrom(cx + changeInX, cy + changeInY), point(cx + changeInX, cy + changeInY),
-originalElement.angle as Radians, -originalElement.angle as Radians,
); );
-188
View File
@@ -1,188 +0,0 @@
import { getShortcutKey, updateActiveTool } from "../utils";
import { t } from "../i18n";
import type { Action } from "../actions/types";
import { makeCustomActionName } from "../actions/types";
import clsx from "clsx";
import type { Subtype, SubtypeRecord } from "../element/subtypes";
import {
getSubtypeNames,
hasAlwaysEnabledActions,
isSubtypeAction,
isValidSubtype,
subtypeCollides,
} from "../element/subtypes";
import type { ExcalidrawElement, Theme } from "../element/types";
import {
useExcalidrawActionManager,
useExcalidrawContainer,
useExcalidrawSetAppState,
} from "./App";
import type { ContextMenuItems } from "./ContextMenu";
import { Island } from "./Island";
export const SubtypeButton = (
subtype: Subtype,
parentType: SubtypeRecord["parents"][number],
icon: ({ theme }: { theme: Theme }) => JSX.Element,
key?: string,
) => {
const title = key !== undefined ? ` - ${getShortcutKey(key)}` : "";
const keyTest: Action["keyTest"] =
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
const subtypeAction: Action = {
name: makeCustomActionName(subtype),
label: t(`toolBar.${subtype}`),
trackEvent: false,
predicate: (...rest) => rest[4]?.subtype === subtype,
perform: (elements, appState) => {
const inactive = !appState.activeSubtypes?.includes(subtype) ?? true;
const activeSubtypes: Subtype[] = [];
if (appState.activeSubtypes) {
activeSubtypes.push(...appState.activeSubtypes);
}
let activated = false;
if (inactive) {
// Ensure `element.subtype` is well-defined
if (!subtypeCollides(subtype, activeSubtypes)) {
activeSubtypes.push(subtype);
activated = true;
}
} else {
// Can only be active if appState.activeSubtypes is defined
// and contains subtype.
activeSubtypes.splice(activeSubtypes.indexOf(subtype), 1);
}
const type =
appState.activeTool.type !== "custom" &&
isValidSubtype(subtype, appState.activeTool.type)
? appState.activeTool.type
: parentType;
const activeTool = !inactive
? appState.activeTool
: updateActiveTool(appState, { type });
const selectedElementIds = activated ? {} : appState.selectedElementIds;
const selectedGroupIds = activated ? {} : appState.selectedGroupIds;
return {
appState: {
...appState,
activeSubtypes,
selectedElementIds,
selectedGroupIds,
activeTool,
},
storeAction: "capture",
};
},
keyTest,
PanelComponent: ({ elements, appState, updateData, data }) => (
<button
className={clsx("ToolIcon_type_button", "ToolIcon_type_button--show", {
ToolIcon: true,
"ToolIcon--selected":
appState.activeSubtypes !== undefined &&
appState.activeSubtypes.includes(subtype),
"ToolIcon--plain": true,
})}
title={`${t(`toolBar.${subtype}`)}${title}`}
aria-label={t(`toolBar.${subtype}`)}
onClick={() => {
updateData(null);
}}
onContextMenu={
data && "onContextMenu" in data
? (event: React.MouseEvent) => {
if (
appState.activeSubtypes === undefined ||
(appState.activeSubtypes !== undefined &&
!appState.activeSubtypes.includes(subtype))
) {
updateData(null);
}
data.onContextMenu(event, subtype);
}
: undefined
}
>
{
<div className="ToolIcon__icon" aria-hidden="true">
{icon.call(this, { theme: appState.theme })}
</div>
}
</button>
),
};
if (key === "") {
delete subtypeAction.keyTest;
}
return subtypeAction;
};
export const SubtypeToggles = () => {
const am = useExcalidrawActionManager();
const { container } = useExcalidrawContainer();
const setAppState = useExcalidrawSetAppState();
const onContextMenu = (
event: React.MouseEvent<HTMLButtonElement>,
subtype: string,
) => {
event.preventDefault();
const { top: offsetTop, left: offsetLeft } =
container!.getBoundingClientRect();
const left = event.clientX - offsetLeft;
const top = event.clientY - offsetTop;
const items: ContextMenuItems = [];
am.filterActions(isSubtypeAction).forEach(
(action) =>
am.isActionEnabled(action, { data: { subtype } }) && items.push(action),
);
setAppState({}, () => {
setAppState({
contextMenu: { top, left, items },
});
});
};
// Only render if one or more subtypes are registered
if (getSubtypeNames().length === 0) {
return <></>;
}
return (
<>
<Island
style={{
marginLeft: 8,
alignSelf: "center",
height: "fit-content",
}}
>
{getSubtypeNames().map((subtype) =>
am.renderAction(
makeCustomActionName(subtype),
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
),
)}
</Island>
</>
);
};
SubtypeToggles.displayName = "SubtypeToggles";
export const SubtypeShapeActions = (props: {
elements: readonly ExcalidrawElement[];
}) => {
const am = useExcalidrawActionManager();
return (
<>
{am
.filterActions(isSubtypeAction, { elements: props.elements })
.map((action) => am.renderAction(action.name))}
</>
);
};
SubtypeShapeActions.displayName = "SubtypeShapeActions";
@@ -36,7 +36,7 @@ import { trackEvent } from "../../analytics";
import { useAppProps, useExcalidrawAppState } from "../App"; import { useAppProps, useExcalidrawAppState } from "../App";
import { isEmbeddableElement } from "../../element/typeChecks"; import { isEmbeddableElement } from "../../element/typeChecks";
import { getLinkHandleFromCoords } from "./helpers"; import { getLinkHandleFromCoords } from "./helpers";
import { pointFrom, type GlobalPoint } from "../../../math"; import { point, type GlobalPoint } from "../../../math";
const CONTAINER_WIDTH = 320; const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85; const SPACE_BOTTOM = 85;
@@ -181,7 +181,7 @@ export const Hyperlink = ({
element, element,
elementsMap, elementsMap,
appState, appState,
pointFrom(event.clientX, event.clientY), point(event.clientX, event.clientY),
) as boolean; ) as boolean;
if (shouldHide) { if (shouldHide) {
timeoutId = window.setTimeout(() => { timeoutId = window.setTimeout(() => {
@@ -1,5 +1,5 @@
import type { GlobalPoint, Radians } from "../../../math"; import type { GlobalPoint, Radians } from "../../../math";
import { pointFrom, pointRotateRads } from "../../../math"; import { point, pointRotateRads } from "../../../math";
import { MIME_TYPES } from "../../constants"; import { MIME_TYPES } from "../../constants";
import type { Bounds } from "../../element/bounds"; import type { Bounds } from "../../element/bounds";
import { getElementAbsoluteCoords } from "../../element/bounds"; import { getElementAbsoluteCoords } from "../../element/bounds";
@@ -35,8 +35,8 @@ export const getLinkHandleFromCoords = (
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset; const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
pointFrom(x + linkWidth / 2, y + linkHeight / 2), point(x + linkWidth / 2, y + linkHeight / 2),
pointFrom(centerX, centerY), point(centerX, centerY),
angle, angle,
); );
return [ return [
@@ -85,10 +85,5 @@ export const isPointHittingLink = (
) { ) {
return true; return true;
} }
return isPointHittingLinkIcon( return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y));
element,
elementsMap,
appState,
pointFrom(x, y),
);
}; };
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "#d8f5a2", "backgroundColor": "#d8f5a2",
"boundElements": [ "boundElements": [
{ {
"id": "id47", "id": "id45",
"type": "arrow", "type": "arrow",
}, },
{ {
"id": "id48", "id": "id46",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id48", "id": "id46",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id49", "elementId": "id47",
"fixedPoint": null, "fixedPoint": null,
"focus": -0.08139534883720931, "focus": -0.08139534883720931,
"gap": 1, "gap": 1,
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id47", "id": "id45",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id50", "id": "id48",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id50", "id": "id48",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id51", "id": "id49",
"type": "text", "type": "text",
}, },
], ],
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id50", "containerId": "id48",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id40", "id": "id38",
"type": "text", "type": "text",
}, },
], ],
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"elbowed": false, "elbowed": false,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id42", "elementId": "id40",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id41", "elementId": "id39",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id39", "containerId": "id37",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id39", "id": "id37",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id39", "id": "id37",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id44", "id": "id42",
"type": "text", "type": "text",
}, },
], ],
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"elbowed": false, "elbowed": false,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id46", "elementId": "id44",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id45", "elementId": "id43",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id43", "containerId": "id41",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id43", "id": "id41",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id43", "id": "id41",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id56", "id": "id54",
"type": "text", "type": "text",
}, },
{ {
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id57", "id": "id55",
"type": "text", "type": "text",
}, },
], ],
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id58", "id": "id56",
"type": "text", "type": "text",
}, },
{ {
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id59", "id": "id57",
"type": "text", "type": "text",
}, },
{ {
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id60", "id": "id58",
"type": "text", "type": "text",
}, },
], ],
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id61", "id": "id59",
"type": "text", "type": "text",
}, },
], ],
+4 -14
View File
@@ -57,7 +57,7 @@ import {
getNormalizedZoom, getNormalizedZoom,
} from "../scene"; } from "../scene";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { isFiniteNumber, pointFrom } from "../../math"; import { isFiniteNumber, point } from "../../math";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -121,8 +121,7 @@ const repairBinding = (
}; };
const restoreElementWithProperties = < const restoreElementWithProperties = <
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & { T extends Required<Omit<ExcalidrawElement, "customData">> & {
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"]; customData?: ExcalidrawElement["customData"];
/** @deprecated */ /** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][]; boundElementIds?: readonly ExcalidrawElement["id"][];
@@ -185,9 +184,6 @@ const restoreElementWithProperties = <
locked: element.locked ?? false, locked: element.locked ?? false,
}; };
if ("subtype" in element) {
base.subtype = element.subtype;
}
if ("customData" in element || "customData" in extra) { if ("customData" in element || "customData" in extra) {
base.customData = base.customData =
"customData" in extra ? extra.customData : element.customData; "customData" in extra ? extra.customData : element.customData;
@@ -272,7 +268,7 @@ const restoreElement = (
let y = element.y; let y = element.y;
let points = // migrate old arrow model to new one let points = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [pointFrom(0, 0), pointFrom(element.width, element.height)] ? [point(0, 0), point(element.width, element.height)]
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
@@ -300,7 +296,7 @@ const restoreElement = (
let y: number | undefined = element.y; let y: number | undefined = element.y;
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [pointFrom(0, 0), pointFrom(element.width, element.height)] ? [point(0, 0), point(element.width, element.height)]
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
@@ -601,12 +597,6 @@ export const restoreAppState = (
: defaultValue; : defaultValue;
} }
if ("activeSubtypes" in appState) {
nextAppState.activeSubtypes = appState.activeSubtypes;
}
if ("customData" in appState) {
nextAppState.customData = appState.customData;
}
return { return {
...nextAppState, ...nextAppState,
cursorButton: localAppState?.cursorButton || "up", cursorButton: localAppState?.cursorButton || "up",
+45 -50
View File
@@ -2,7 +2,7 @@ import { vi } from "vitest";
import type { ExcalidrawElementSkeleton } from "./transform"; import type { ExcalidrawElementSkeleton } from "./transform";
import { convertToExcalidrawElements } from "./transform"; import { convertToExcalidrawElements } from "./transform";
import type { ExcalidrawArrowElement } from "../element/types"; import type { ExcalidrawArrowElement } from "../element/types";
import { pointFrom } from "../../math"; import { point } from "../../math";
const opts = { regenerateIds: false }; const opts = { regenerateIds: false };
@@ -309,32 +309,28 @@ describe("Test Transform", () => {
}); });
describe("Test Frames", () => { describe("Test Frames", () => {
const elements: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
];
it("should transform frames and update frame ids when regenerated", () => { it("should transform frames and update frame ids when regenerated", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [ const elementsSkeleton: ExcalidrawElementSkeleton[] = [
...elements, {
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{ {
type: "frame", type: "frame",
children: ["1", "2"], children: ["1", "2"],
@@ -356,9 +352,28 @@ describe("Test Transform", () => {
}); });
}); });
it("should consider user defined frame dimensions over calculated when provided", () => { it("should consider max of calculated and frame dimensions when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [ const elementsSkeleton: ExcalidrawElementSkeleton[] = [
...elements, {
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{ {
type: "frame", type: "frame",
children: ["1", "2"], children: ["1", "2"],
@@ -373,27 +388,7 @@ describe("Test Transform", () => {
); );
const frame = excalidrawElements.find((ele) => ele.type === "frame")!; const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.width).toBe(800); expect(frame.width).toBe(800);
expect(frame.height).toBe(100); expect(frame.height).toBe(126);
});
it("should consider user defined frame coordinates calculated when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
...elements,
{
type: "frame",
children: ["1", "2"],
name: "My frame",
x: 100,
y: 300,
},
];
const excalidrawElements = convertToExcalidrawElements(
elementsSkeleton,
opts,
);
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.x).toBe(100);
expect(frame.y).toBe(300);
}); });
}); });
@@ -917,7 +912,7 @@ describe("Test Transform", () => {
x: 111.262, x: 111.262,
y: 57, y: 57,
strokeWidth: 2, strokeWidth: 2,
points: [pointFrom(0, 0), pointFrom(272.985, 0)], points: [point(0, 0), point(272.985, 0)],
label: { label: {
text: "How are you?", text: "How are you?",
fontSize: 20, fontSize: 20,
@@ -940,7 +935,7 @@ describe("Test Transform", () => {
x: 77.017, x: 77.017,
y: 79, y: 79,
strokeWidth: 2, strokeWidth: 2,
points: [pointFrom(0, 0)], points: [point(0, 0)],
label: { label: {
text: "Friendship", text: "Friendship",
fontSize: 20, fontSize: 20,
+8 -25
View File
@@ -46,7 +46,6 @@ import {
assertNever, assertNever,
cloneJSON, cloneJSON,
getFontString, getFontString,
isDevEnv,
toBrandedType, toBrandedType,
} from "../utils"; } from "../utils";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
@@ -54,7 +53,7 @@ import { randomId } from "../random";
import { syncInvalidIndices } from "../fractionalIndex"; import { syncInvalidIndices } from "../fractionalIndex";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import { isArrowElement } from "../element/typeChecks"; import { isArrowElement } from "../element/typeChecks";
import { pointFrom, type LocalPoint } from "../../math"; import { point, type LocalPoint } from "../../math";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
@@ -537,7 +536,7 @@ export const convertToExcalidrawElements = (
excalidrawElement = newLinearElement({ excalidrawElement = newLinearElement({
width, width,
height, height,
points: [pointFrom(0, 0), pointFrom(width, height)], points: [point(0, 0), point(width, height)],
...element, ...element,
}); });
@@ -550,7 +549,7 @@ export const convertToExcalidrawElements = (
width, width,
height, height,
endArrowhead: "arrow", endArrowhead: "arrow",
points: [pointFrom(0, 0), pointFrom(width, height)], points: [point(0, 0), point(width, height)],
...element, ...element,
type: "arrow", type: "arrow",
}); });
@@ -718,7 +717,7 @@ export const convertToExcalidrawElements = (
} }
// Once all the excalidraw elements are created, we can add frames since we // Once all the excalidraw elements are created, we can add frames since we
// need to calculate coordinates and dimensions of frame which is possible after all // need to calculate coordinates and dimensions of frame which is possibe after all
// frame children are processed. // frame children are processed.
for (const [id, element] of elementsWithIds) { for (const [id, element] of elementsWithIds) {
if (element.type !== "frame" && element.type !== "magicframe") { if (element.type !== "frame" && element.type !== "magicframe") {
@@ -765,26 +764,10 @@ export const convertToExcalidrawElements = (
maxX = maxX + PADDING; maxX = maxX + PADDING;
maxY = maxY + PADDING; maxY = maxY + PADDING;
const frameX = frame?.x || minX; // Take the max of calculated and provided frame dimensions, whichever is higher
const frameY = frame?.y || minY; const width = Math.max(frame?.width, maxX - minX);
const frameWidth = frame?.width || maxX - minX; const height = Math.max(frame?.height, maxY - minY);
const frameHeight = frame?.height || maxY - minY; Object.assign(frame, { x: minX, y: minY, width, height });
Object.assign(frame, {
x: frameX,
y: frameY,
width: frameWidth,
height: frameHeight,
});
if (
isDevEnv() &&
element.children.length &&
(frame?.x || frame?.y || frame?.width || frame?.height)
) {
console.info(
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
);
}
} }
return elementStore.getElements(); return elementStore.getElements();
+33 -42
View File
@@ -66,7 +66,7 @@ import {
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { import {
lineSegment, lineSegment,
pointFrom, point,
pointRotateRads, pointRotateRads,
type GlobalPoint, type GlobalPoint,
vectorFromPoint, vectorFromPoint,
@@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
return vectorToHeading( return vectorToHeading(
vectorFromPoint( vectorFromPoint(
p, p,
pointFrom<GlobalPoint>( point<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2, bindableElement.y + bindableElement.height / 2,
), ),
@@ -766,15 +766,15 @@ export const bindPointToSnapToElementOutline = (
const intersections = [ const intersections = [
...(intersectElementWithLine( ...(intersectElementWithLine(
bindableElement, bindableElement,
pointFrom(p[0], p[1] - 2 * bindableElement.height), point(p[0], p[1] - 2 * bindableElement.height),
pointFrom(p[0], p[1] + 2 * bindableElement.height), point(p[0], p[1] + 2 * bindableElement.height),
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
) ?? []), ) ?? []),
...(intersectElementWithLine( ...(intersectElementWithLine(
bindableElement, bindableElement,
pointFrom(p[0] - 2 * bindableElement.width, p[1]), point(p[0] - 2 * bindableElement.width, p[1]),
pointFrom(p[0] + 2 * bindableElement.width, p[1]), point(p[0] + 2 * bindableElement.width, p[1]),
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
) ?? []), ) ?? []),
@@ -815,25 +815,25 @@ const headingToMidBindPoint = (
switch (true) { switch (true) {
case compareHeading(heading, HEADING_UP): case compareHeading(heading, HEADING_UP):
return pointRotateRads( return pointRotateRads(
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]), point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
center, center,
bindableElement.angle, bindableElement.angle,
); );
case compareHeading(heading, HEADING_RIGHT): case compareHeading(heading, HEADING_RIGHT):
return pointRotateRads( return pointRotateRads(
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1), point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
center, center,
bindableElement.angle, bindableElement.angle,
); );
case compareHeading(heading, HEADING_DOWN): case compareHeading(heading, HEADING_DOWN):
return pointRotateRads( return pointRotateRads(
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]), point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
center, center,
bindableElement.angle, bindableElement.angle,
); );
default: default:
return pointRotateRads( return pointRotateRads(
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1), point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
center, center,
bindableElement.angle, bindableElement.angle,
); );
@@ -844,7 +844,7 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = pointFrom<GlobalPoint>( const center = point<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
); );
@@ -854,13 +854,13 @@ export const avoidRectangularCorner = (
// Top left // Top left
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) { if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
return pointRotateRads<GlobalPoint>( return pointRotateRads<GlobalPoint>(
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y), point(element.x - FIXED_BINDING_DISTANCE, element.y),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE), point(element.x, element.y - FIXED_BINDING_DISTANCE),
center, center,
element.angle, element.angle,
); );
@@ -871,16 +871,13 @@ export const avoidRectangularCorner = (
// Bottom left // Bottom left
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) { if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
return pointRotateRads( return pointRotateRads(
pointFrom( point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
element.x,
element.y + element.height + FIXED_BINDING_DISTANCE,
),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height), point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
center, center,
element.angle, element.angle,
); );
@@ -894,7 +891,7 @@ export const avoidRectangularCorner = (
element.width + FIXED_BINDING_DISTANCE element.width + FIXED_BINDING_DISTANCE
) { ) {
return pointRotateRads( return pointRotateRads(
pointFrom( point(
element.x + element.width, element.x + element.width,
element.y + element.height + FIXED_BINDING_DISTANCE, element.y + element.height + FIXED_BINDING_DISTANCE,
), ),
@@ -903,7 +900,7 @@ export const avoidRectangularCorner = (
); );
} }
return pointRotateRads( return pointRotateRads(
pointFrom( point(
element.x + element.width + FIXED_BINDING_DISTANCE, element.x + element.width + FIXED_BINDING_DISTANCE,
element.y + element.height, element.y + element.height,
), ),
@@ -920,16 +917,13 @@ export const avoidRectangularCorner = (
element.width + FIXED_BINDING_DISTANCE element.width + FIXED_BINDING_DISTANCE
) { ) {
return pointRotateRads( return pointRotateRads(
pointFrom( point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
element.x + element.width,
element.y - FIXED_BINDING_DISTANCE,
),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y), point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
center, center,
element.angle, element.angle,
); );
@@ -944,10 +938,7 @@ export const snapToMid = (
tolerance: number = 0.05, tolerance: number = 0.05,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, height, angle } = element;
const center = pointFrom<GlobalPoint>( const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
x + width / 2 - 0.1,
y + height / 2 - 0.1,
);
const nonRotated = pointRotateRads(p, center, -angle as Radians); const nonRotated = pointRotateRads(p, center, -angle as Radians);
// snap-to-center point is adaptive to element size, but we don't want to go // snap-to-center point is adaptive to element size, but we don't want to go
@@ -962,7 +953,7 @@ export const snapToMid = (
) { ) {
// LEFT // LEFT
return pointRotateRads( return pointRotateRads(
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]), point(x - FIXED_BINDING_DISTANCE, center[1]),
center, center,
angle, angle,
); );
@@ -973,7 +964,7 @@ export const snapToMid = (
) { ) {
// TOP // TOP
return pointRotateRads( return pointRotateRads(
pointFrom(center[0], y - FIXED_BINDING_DISTANCE), point(center[0], y - FIXED_BINDING_DISTANCE),
center, center,
angle, angle,
); );
@@ -984,7 +975,7 @@ export const snapToMid = (
) { ) {
// RIGHT // RIGHT
return pointRotateRads( return pointRotateRads(
pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]), point(x + width + FIXED_BINDING_DISTANCE, center[1]),
center, center,
angle, angle,
); );
@@ -995,7 +986,7 @@ export const snapToMid = (
) { ) {
// DOWN // DOWN
return pointRotateRads( return pointRotateRads(
pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE), point(center[0], y + height + FIXED_BINDING_DISTANCE),
center, center,
angle, angle,
); );
@@ -1032,11 +1023,11 @@ const updateBoundPoint = (
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap, elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>( const globalMidPoint = point<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2, bindableElement.y + bindableElement.height / 2,
); );
const global = pointFrom<GlobalPoint>( const global = point<GlobalPoint>(
bindableElement.x + fixedPoint[0] * bindableElement.width, bindableElement.x + fixedPoint[0] * bindableElement.width,
bindableElement.y + fixedPoint[1] * bindableElement.height, bindableElement.y + fixedPoint[1] * bindableElement.height,
); );
@@ -1127,7 +1118,7 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement, hoveredElement,
elementsMap, elementsMap,
); );
const globalMidPoint = pointFrom( const globalMidPoint = point(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
); );
@@ -1346,9 +1337,9 @@ export const bindingBorderTest = (
const threshold = maxBindingGap(element, element.width, element.height); const threshold = maxBindingGap(element, element.width, element.height);
const shape = getElementShape(element, elementsMap); const shape = getElementShape(element, elementsMap);
return ( return (
isPointOnShape(pointFrom(x, y), shape, threshold) || isPointOnShape(point(x, y), shape, threshold) ||
(fullShape === true && (fullShape === true &&
pointInsideBounds(pointFrom(x, y), aabbForElement(element))) pointInsideBounds(point(x, y), aabbForElement(element)))
); );
}; };
@@ -2206,11 +2197,11 @@ export const getGlobalFixedPointForBindableElement = (
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio); const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
return pointRotateRads( return pointRotateRads(
pointFrom( point(
element.x + element.width * fixedX, element.x + element.width * fixedX,
element.y + element.height * fixedY, element.y + element.height * fixedY,
), ),
pointFrom<GlobalPoint>( point<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
), ),
@@ -2238,7 +2229,7 @@ const getGlobalFixedPoints = (
arrow.startBinding.fixedPoint, arrow.startBinding.fixedPoint,
startElement as ExcalidrawBindableElement, startElement as ExcalidrawBindableElement,
) )
: pointFrom<GlobalPoint>( : point<GlobalPoint>(
arrow.x + arrow.points[0][0], arrow.x + arrow.points[0][0],
arrow.y + arrow.points[0][1], arrow.y + arrow.points[0][1],
); );
@@ -2248,7 +2239,7 @@ const getGlobalFixedPoints = (
arrow.endBinding.fixedPoint, arrow.endBinding.fixedPoint,
endElement as ExcalidrawBindableElement, endElement as ExcalidrawBindableElement,
) )
: pointFrom<GlobalPoint>( : point<GlobalPoint>(
arrow.x + arrow.points[arrow.points.length - 1][0], arrow.x + arrow.points[arrow.points.length - 1][0],
arrow.y + arrow.points[arrow.points.length - 1][1], arrow.y + arrow.points[arrow.points.length - 1][1],
); );
+4 -4
View File
@@ -1,5 +1,5 @@
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom } from "../../math"; import { point } from "../../math";
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
@@ -125,9 +125,9 @@ describe("getElementBounds", () => {
a: 0.6447741904932416, a: 0.6447741904932416,
}), }),
points: [ points: [
pointFrom<LocalPoint>(0, 0), point<LocalPoint>(0, 0),
pointFrom<LocalPoint>(67.33984375, 92.48828125), point<LocalPoint>(67.33984375, 92.48828125),
pointFrom<LocalPoint>(-102.7890625, 52.15625), point<LocalPoint>(-102.7890625, 52.15625),
], ],
} as ExcalidrawLinearElement; } as ExcalidrawLinearElement;
+46 -46
View File
@@ -34,7 +34,7 @@ import type {
import { import {
degreesToRadians, degreesToRadians,
lineSegment, lineSegment,
pointFrom, point,
pointDistance, pointDistance,
pointFromArray, pointFromArray,
pointRotateRads, pointRotateRads,
@@ -113,8 +113,8 @@ export class ElementBounds {
const [minX, minY, maxX, maxY] = getBoundsFromPoints( const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) => element.points.map(([x, y]) =>
pointRotateRads( pointRotateRads(
pointFrom(x, y), point(x, y),
pointFrom(cx - element.x, cy - element.y), point(cx - element.x, cy - element.y),
element.angle, element.angle,
), ),
), ),
@@ -130,23 +130,23 @@ export class ElementBounds {
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
} else if (element.type === "diamond") { } else if (element.type === "diamond") {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
pointFrom(cx, y1), point(cx, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x12, y12] = pointRotateRads( const [x12, y12] = pointRotateRads(
pointFrom(cx, y2), point(cx, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x22, y22] = pointRotateRads( const [x22, y22] = pointRotateRads(
pointFrom(x1, cy), point(x1, cy),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x21, y21] = pointRotateRads( const [x21, y21] = pointRotateRads(
pointFrom(x2, cy), point(x2, cy),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const minX = Math.min(x11, x12, x22, x21); const minX = Math.min(x11, x12, x22, x21);
@@ -164,23 +164,23 @@ export class ElementBounds {
bounds = [cx - ww, cy - hh, cx + ww, cy + hh]; bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
} else { } else {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
pointFrom(x1, y1), point(x1, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x12, y12] = pointRotateRads( const [x12, y12] = pointRotateRads(
pointFrom(x1, y2), point(x1, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x22, y22] = pointRotateRads( const [x22, y22] = pointRotateRads(
pointFrom(x2, y2), point(x2, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const [x21, y21] = pointRotateRads( const [x21, y21] = pointRotateRads(
pointFrom(x2, y1), point(x2, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const minX = Math.min(x11, x12, x22, x21); const minX = Math.min(x11, x12, x22, x21);
@@ -255,7 +255,7 @@ export const getElementLineSegments = (
elementsMap, elementsMap,
); );
const center: GlobalPoint = pointFrom(cx, cy); const center: GlobalPoint = point(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = []; const segments: LineSegment<GlobalPoint>[] = [];
@@ -266,7 +266,7 @@ export const getElementLineSegments = (
segments.push( segments.push(
lineSegment( lineSegment(
pointRotateRads( pointRotateRads(
pointFrom( point(
element.points[i][0] + element.x, element.points[i][0] + element.x,
element.points[i][1] + element.y, element.points[i][1] + element.y,
), ),
@@ -274,7 +274,7 @@ export const getElementLineSegments = (
element.angle, element.angle,
), ),
pointRotateRads( pointRotateRads(
pointFrom( point(
element.points[i + 1][0] + element.x, element.points[i + 1][0] + element.x,
element.points[i + 1][1] + element.y, element.points[i + 1][1] + element.y,
), ),
@@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = (
ops: Op[], ops: Op[],
transformXY?: (p: GlobalPoint) => GlobalPoint, transformXY?: (p: GlobalPoint) => GlobalPoint,
): Bounds => { ): Bounds => {
let currentP: GlobalPoint = pointFrom(0, 0); let currentP: GlobalPoint = point(0, 0);
const { minX, minY, maxX, maxY } = ops.reduce( const { minX, minY, maxX, maxY } = ops.reduce(
(limits, { op, data }) => { (limits, { op, data }) => {
@@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = (
// move operation does not draw anything; so, it always // move operation does not draw anything; so, it always
// returns false // returns false
} else if (op === "bcurveTo") { } else if (op === "bcurveTo") {
const _p1 = pointFrom<GlobalPoint>(data[0], data[1]); const _p1 = point<GlobalPoint>(data[0], data[1]);
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]); const _p2 = point<GlobalPoint>(data[2], data[3]);
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]); const _p3 = point<GlobalPoint>(data[4], data[5]);
const p1 = transformXY ? transformXY(_p1) : _p1; const p1 = transformXY ? transformXY(_p1) : _p1;
const p2 = transformXY ? transformXY(_p2) : _p2; const p2 = transformXY ? transformXY(_p2) : _p2;
@@ -591,21 +591,21 @@ export const getArrowheadPoints = (
invariant(data.length === 6, "Op data length is not 6"); invariant(data.length === 6, "Op data length is not 6");
const p3 = pointFrom(data[4], data[5]); const p3 = point(data[4], data[5]);
const p2 = pointFrom(data[2], data[3]); const p2 = point(data[2], data[3]);
const p1 = pointFrom(data[0], data[1]); const p1 = point(data[0], data[1]);
// We need to find p0 of the bezier curve. // We need to find p0 of the bezier curve.
// It is typically the last point of the previous // It is typically the last point of the previous
// curve; it can also be the position of moveTo operation. // curve; it can also be the position of moveTo operation.
const prevOp = ops[index - 1]; const prevOp = ops[index - 1];
let p0 = pointFrom(0, 0); let p0 = point(0, 0);
if (prevOp.op === "move") { if (prevOp.op === "move") {
const p = pointFromArray(prevOp.data); const p = pointFromArray(prevOp.data);
invariant(p != null, "Op data is not a point"); invariant(p != null, "Op data is not a point");
p0 = p; p0 = p;
} else if (prevOp.op === "bcurveTo") { } else if (prevOp.op === "bcurveTo") {
p0 = pointFrom(prevOp.data[4], prevOp.data[5]); p0 = point(prevOp.data[4], prevOp.data[5]);
} }
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
@@ -671,13 +671,13 @@ export const getArrowheadPoints = (
// Return points // Return points
const [x3, y3] = pointRotateRads( const [x3, y3] = pointRotateRads(
pointFrom(xs, ys), point(xs, ys),
pointFrom(x2, y2), point(x2, y2),
((-angle * Math.PI) / 180) as Radians, ((-angle * Math.PI) / 180) as Radians,
); );
const [x4, y4] = pointRotateRads( const [x4, y4] = pointRotateRads(
pointFrom(xs, ys), point(xs, ys),
pointFrom(x2, y2), point(x2, y2),
degreesToRadians(angle), degreesToRadians(angle),
); );
@@ -690,8 +690,8 @@ export const getArrowheadPoints = (
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0]; const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
[ox, oy] = pointRotateRads( [ox, oy] = pointRotateRads(
pointFrom(x2 + minSize * 2, y2), point(x2 + minSize * 2, y2),
pointFrom(x2, y2), point(x2, y2),
Math.atan2(py - y2, px - x2) as Radians, Math.atan2(py - y2, px - x2) as Radians,
); );
} else { } else {
@@ -701,8 +701,8 @@ export const getArrowheadPoints = (
: [0, 0]; : [0, 0];
[ox, oy] = pointRotateRads( [ox, oy] = pointRotateRads(
pointFrom(x2 - minSize * 2, y2), point(x2 - minSize * 2, y2),
pointFrom(x2, y2), point(x2, y2),
Math.atan2(y2 - py, x2 - px) as Radians, Math.atan2(y2 - py, x2 - px) as Radians,
); );
} }
@@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = (
if (element.points.length < 2) { if (element.points.length < 2) {
const [pointX, pointY] = element.points[0]; const [pointX, pointY] = element.points[0];
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
pointFrom(element.x + pointX, element.y + pointY), point(element.x + pointX, element.y + pointY),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
@@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = (
const ops = getCurvePathOps(shape); const ops = getCurvePathOps(shape);
const transformXY = ([x, y]: GlobalPoint) => const transformXY = ([x, y]: GlobalPoint) =>
pointRotateRads<GlobalPoint>( pointRotateRads<GlobalPoint>(
pointFrom(element.x + x, element.y + y), point(element.x + x, element.y + y),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const res = getMinMaxXYFromCurvePathOps(ops, transformXY); const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
@@ -931,8 +931,8 @@ export const getClosestElementBounds = (
elements.forEach((element) => { elements.forEach((element) => {
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
const distance = pointDistance( const distance = pointDistance(
pointFrom((x1 + x2) / 2, (y1 + y2) / 2), point((x1 + x2) / 2, (y1 + y2) / 2),
pointFrom(from.x, from.y), point(from.x, from.y),
); );
if (distance < minDistance) { if (distance < minDistance) {
@@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({
}; };
export const getCenterForBounds = (bounds: Bounds): GlobalPoint => export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
pointFrom( point(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
); );
+7 -11
View File
@@ -17,7 +17,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { getBoundTextShape, isPathALoop } from "../shapes"; import { getBoundTextShape, isPathALoop } from "../shapes";
import type { GlobalPoint, LocalPoint, Polygon } from "../../math"; import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
import { isPointWithinBounds, pointFrom } from "../../math"; import { isPointWithinBounds, point } from "../../math";
export const shouldTestInside = (element: ExcalidrawElement) => { export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") { if (element.type === "arrow") {
@@ -61,13 +61,13 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
let hit = shouldTestInside(element) let hit = shouldTestInside(element)
? // Since `inShape` tests STRICTLY againt the insides of a shape ? // Since `inShape` tests STRICTLY againt the insides of a shape
// we would need `onShape` as well to include the "borders" // we would need `onShape` as well to include the "borders"
isPointInShape(pointFrom(x, y), shape) || isPointInShape(point(x, y), shape) ||
isPointOnShape(pointFrom(x, y), shape, threshold) isPointOnShape(point(x, y), shape, threshold)
: isPointOnShape(pointFrom(x, y), shape, threshold); : isPointOnShape(point(x, y), shape, threshold);
// hit test against a frame's name // hit test against a frame's name
if (!hit && frameNameBound) { if (!hit && frameNameBound) {
hit = isPointInShape(pointFrom(x, y), { hit = isPointInShape(point(x, y), {
type: "polygon", type: "polygon",
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
.data as Polygon<Point>, .data as Polygon<Point>,
@@ -89,11 +89,7 @@ export const hitElementBoundingBox = (
y1 -= tolerance; y1 -= tolerance;
x2 += tolerance; x2 += tolerance;
y2 += tolerance; y2 += tolerance;
return isPointWithinBounds( return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
pointFrom(x1, y1),
pointFrom(x, y),
pointFrom(x2, y2),
);
}; };
export const hitElementBoundingBoxOnly = < export const hitElementBoundingBoxOnly = <
@@ -119,5 +115,5 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
y: number, y: number,
textShape: GeometricShape<Point> | null, textShape: GeometricShape<Point> | null,
): boolean => { ): boolean => {
return !!textShape && isPointInShape(pointFrom(x, y), textShape); return !!textShape && isPointInShape(point(x, y), textShape);
}; };
-31
View File
@@ -45,12 +45,6 @@ const RE_GENERIC_EMBED =
const RE_GIPHY = const RE_GIPHY =
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/; /giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
const RE_REDDIT =
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const ALLOWED_DOMAINS = new Set([ const ALLOWED_DOMAINS = new Set([
"youtube.com", "youtube.com",
"youtu.be", "youtu.be",
@@ -65,7 +59,6 @@ const ALLOWED_DOMAINS = new Set([
"stackblitz.com", "stackblitz.com",
"val.town", "val.town",
"giphy.com", "giphy.com",
"reddit.com",
]); ]);
const ALLOW_SAME_ORIGIN = new Set([ const ALLOW_SAME_ORIGIN = new Set([
@@ -78,7 +71,6 @@ const ALLOW_SAME_ORIGIN = new Set([
"x.com", "x.com",
"*.simplepdf.eu", "*.simplepdf.eu",
"stackblitz.com", "stackblitz.com",
"reddit.com",
]); ]);
export const createSrcDoc = (body: string) => { export const createSrcDoc = (body: string) => {
@@ -226,24 +218,6 @@ export const getEmbedLink = (
return ret; return ret;
} }
if (RE_REDDIT.test(link)) {
const [, page, postId, title] = link.match(RE_REDDIT)!;
const safeURL = sanitizeHTMLAttribute(
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
);
const ret: IframeDataWithSandbox = {
type: "document",
srcdoc: (theme: string) =>
createSrcDoc(
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
),
intrinsicSize: { w: 480, h: 480 },
sandbox: { allowSameOrigin },
};
embeddedLinkCache.set(originalLink, ret);
return ret;
}
if (RE_GH_GIST.test(link)) { if (RE_GH_GIST.test(link)) {
const [, user, gistId] = link.match(RE_GH_GIST)!; const [, user, gistId] = link.match(RE_GH_GIST)!;
const safeURL = sanitizeHTMLAttribute( const safeURL = sanitizeHTMLAttribute(
@@ -387,11 +361,6 @@ export const maybeParseEmbedSrc = (str: string): string => {
return twitterMatch[1]; return twitterMatch[1];
} }
const redditMatch = str.match(RE_REDDIT_EMBED);
if (redditMatch && redditMatch.length === 2) {
return redditMatch[1];
}
const gistMatch = str.match(RE_GH_GIST_EMBED); const gistMatch = str.match(RE_GH_GIST_EMBED);
if (gistMatch && gistMatch.length === 2) { if (gistMatch && gistMatch.length === 2) {
return gistMatch[1]; return gistMatch[1];
+2 -2
View File
@@ -29,7 +29,7 @@ import {
isFlowchartNodeElement, isFlowchartNodeElement,
} from "./typeChecks"; } from "./typeChecks";
import { invariant } from "../utils"; import { invariant } from "../utils";
import { pointFrom, type LocalPoint } from "../../math"; import { point, type LocalPoint } from "../../math";
import { aabbForElement } from "../shapes"; import { aabbForElement } from "../shapes";
type LinkDirection = "up" | "right" | "down" | "left"; type LinkDirection = "up" | "right" | "down" | "left";
@@ -421,7 +421,7 @@ const createBindingArrow = (
strokeColor: appState.currentItemStrokeColor, strokeColor: appState.currentItemStrokeColor,
strokeStyle: appState.currentItemStrokeStyle, strokeStyle: appState.currentItemStrokeStyle,
strokeWidth: appState.currentItemStrokeWidth, strokeWidth: appState.currentItemStrokeWidth,
points: [pointFrom(0, 0), pointFrom(endX, endY)], points: [point(0, 0), point(endX, endY)],
elbowed: true, elbowed: true,
}); });
+9 -9
View File
@@ -6,7 +6,7 @@ import type {
Radians, Radians,
} from "../../math"; } from "../../math";
import { import {
pointFrom, point,
pointRotateRads, pointRotateRads,
pointScaleFromOrigin, pointScaleFromOrigin,
radiansToDegrees, radiansToDegrees,
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
const top = pointRotateRads( const top = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
pointFrom(element.x + element.width / 2, element.y), point(element.x + element.width / 2, element.y),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -91,7 +91,7 @@ export const headingForPointFromElement = <
); );
const right = pointRotateRads( const right = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
pointFrom(element.x + element.width, element.y + element.height / 2), point(element.x + element.width, element.y + element.height / 2),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -100,7 +100,7 @@ export const headingForPointFromElement = <
); );
const bottom = pointRotateRads( const bottom = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
pointFrom(element.x + element.width / 2, element.y + element.height), point(element.x + element.width / 2, element.y + element.height),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -109,7 +109,7 @@ export const headingForPointFromElement = <
); );
const left = pointRotateRads( const left = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
pointFrom(element.x, element.y + element.height / 2), point(element.x, element.y + element.height / 2),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -133,22 +133,22 @@ export const headingForPointFromElement = <
} }
const topLeft = pointScaleFromOrigin( const topLeft = pointScaleFromOrigin(
pointFrom(aabb[0], aabb[1]), point(aabb[0], aabb[1]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const topRight = pointScaleFromOrigin( const topRight = pointScaleFromOrigin(
pointFrom(aabb[2], aabb[1]), point(aabb[2], aabb[1]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const bottomLeft = pointScaleFromOrigin( const bottomLeft = pointScaleFromOrigin(
pointFrom(aabb[0], aabb[3]), point(aabb[0], aabb[3]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const bottomRight = pointScaleFromOrigin( const bottomRight = pointScaleFromOrigin(
pointFrom(aabb[2], aabb[3]), point(aabb[2], aabb[3]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
@@ -49,7 +49,7 @@ import type Scene from "../scene/Scene";
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { import {
pointCenter, pointCenter,
pointFrom, point,
pointRotateRads, pointRotateRads,
pointsEqual, pointsEqual,
vector, vector,
@@ -108,7 +108,7 @@ export class LinearElementEditor {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
if (!pointsEqual(element.points[0], pointFrom(0, 0))) { if (!pointsEqual(element.points[0], point(0, 0))) {
console.error("Linear element is not normalized", Error().stack); console.error("Linear element is not normalized", Error().stack);
} }
@@ -287,7 +287,7 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
referencePoint, referencePoint,
pointFrom(scenePointerX, scenePointerY), point(scenePointerX, scenePointerY),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
@@ -296,7 +296,7 @@ export class LinearElementEditor {
[ [
{ {
index: selectedIndex, index: selectedIndex,
point: pointFrom( point: point(
width + referencePoint[0], width + referencePoint[0],
height + referencePoint[1], height + referencePoint[1],
), ),
@@ -329,7 +329,7 @@ export class LinearElementEditor {
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
) )
: pointFrom( : point(
element.points[pointIndex][0] + deltaX, element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY, element.points[pointIndex][1] + deltaY,
); );
@@ -590,11 +590,11 @@ export class LinearElementEditor {
linearElementEditor.segmentMidPointHoveredCoords; linearElementEditor.segmentMidPointHoveredCoords;
if (existingSegmentMidpointHitCoords) { if (existingSegmentMidpointHitCoords) {
const distance = pointDistance( const distance = pointDistance(
pointFrom( point(
existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[0],
existingSegmentMidpointHitCoords[1], existingSegmentMidpointHitCoords[1],
), ),
pointFrom(scenePointer.x, scenePointer.y), point(scenePointer.x, scenePointer.y),
); );
if (distance <= threshold) { if (distance <= threshold) {
return existingSegmentMidpointHitCoords; return existingSegmentMidpointHitCoords;
@@ -606,8 +606,8 @@ export class LinearElementEditor {
while (index < midPoints.length) { while (index < midPoints.length) {
if (midPoints[index] !== null) { if (midPoints[index] !== null) {
const distance = pointDistance( const distance = pointDistance(
pointFrom(midPoints[index]![0], midPoints[index]![1]), point(midPoints[index]![0], midPoints[index]![1]),
pointFrom(scenePointer.x, scenePointer.y), point(scenePointer.x, scenePointer.y),
); );
if (distance <= threshold) { if (distance <= threshold) {
return midPoints[index]; return midPoints[index];
@@ -626,8 +626,8 @@ export class LinearElementEditor {
zoom: AppState["zoom"], zoom: AppState["zoom"],
) { ) {
let distance = pointDistance( let distance = pointDistance(
pointFrom(startPoint[0], startPoint[1]), point(startPoint[0], startPoint[1]),
pointFrom(endPoint[0], endPoint[1]), point(endPoint[0], endPoint[1]),
); );
if (element.points.length > 2 && element.roundness) { if (element.points.length > 2 && element.roundness) {
distance = getBezierCurveLength(element, endPoint); distance = getBezierCurveLength(element, endPoint);
@@ -829,11 +829,11 @@ export class LinearElementEditor {
const targetPoint = const targetPoint =
clickedPointIndex > -1 && clickedPointIndex > -1 &&
pointRotateRads( pointRotateRads(
pointFrom( point(
element.x + element.points[clickedPointIndex][0], element.x + element.points[clickedPointIndex][0],
element.y + element.points[clickedPointIndex][1], element.y + element.points[clickedPointIndex][1],
), ),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
@@ -928,11 +928,11 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
lastCommittedPoint, lastCommittedPoint,
pointFrom(scenePointerX, scenePointerY), point(scenePointerX, scenePointerY),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
newPoint = pointFrom( newPoint = point(
width + lastCommittedPoint[0], width + lastCommittedPoint[0],
height + lastCommittedPoint[1], height + lastCommittedPoint[1],
); );
@@ -984,8 +984,8 @@ export class LinearElementEditor {
const { x, y } = element; const { x, y } = element;
return pointRotateRads( return pointRotateRads(
pointFrom(x + p[0], y + p[1]), point(x + p[0], y + p[1]),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
} }
@@ -1001,8 +1001,8 @@ export class LinearElementEditor {
return element.points.map((p) => { return element.points.map((p) => {
const { x, y } = element; const { x, y } = element;
return pointRotateRads( return pointRotateRads(
pointFrom(x + p[0], y + p[1]), point(x + p[0], y + p[1]),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
}); });
@@ -1025,12 +1025,8 @@ export class LinearElementEditor {
const { x, y } = element; const { x, y } = element;
return p return p
? pointRotateRads( ? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
pointFrom(x + p[0], y + p[1]), : pointRotateRads(point(x, y), point(cx, cy), element.angle);
pointFrom(cx, cy),
element.angle,
)
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
} }
static pointFromAbsoluteCoords( static pointFromAbsoluteCoords(
@@ -1040,7 +1036,7 @@ export class LinearElementEditor {
): LocalPoint { ): LocalPoint {
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
// No rotation for elbow arrows // No rotation for elbow arrows
return pointFrom( return point(
absoluteCoords[0] - element.x, absoluteCoords[0] - element.x,
absoluteCoords[1] - element.y, absoluteCoords[1] - element.y,
); );
@@ -1050,11 +1046,11 @@ export class LinearElementEditor {
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
pointFrom(absoluteCoords[0], absoluteCoords[1]), point(absoluteCoords[0], absoluteCoords[1]),
pointFrom(cx, cy), point(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
return pointFrom(x - element.x, y - element.y); return point(x - element.x, y - element.y);
} }
static getPointIndexUnderCursor( static getPointIndexUnderCursor(
@@ -1075,7 +1071,7 @@ export class LinearElementEditor {
while (--idx > -1) { while (--idx > -1) {
const p = pointHandles[idx]; const p = pointHandles[idx];
if ( if (
pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value < pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
// +1px to account for outline stroke // +1px to account for outline stroke
LinearElementEditor.POINT_HANDLE_SIZE + 1 LinearElementEditor.POINT_HANDLE_SIZE + 1
) { ) {
@@ -1097,12 +1093,12 @@ export class LinearElementEditor {
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
pointFrom(pointerOnGrid[0], pointerOnGrid[1]), point(pointerOnGrid[0], pointerOnGrid[1]),
pointFrom(cx, cy), point(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
return pointFrom(rotatedX - element.x, rotatedY - element.y); return point(rotatedX - element.x, rotatedY - element.y);
} }
/** /**
@@ -1122,7 +1118,7 @@ export class LinearElementEditor {
return { return {
points: points.map((p) => { points: points.map((p) => {
return pointFrom(p[0] - offsetX, p[1] - offsetY); return point(p[0] - offsetX, p[1] - offsetY);
}), }),
x: element.x + offsetX, x: element.x + offsetX,
y: element.y + offsetY, y: element.y + offsetY,
@@ -1176,8 +1172,8 @@ export class LinearElementEditor {
} }
acc.push( acc.push(
nextPoint nextPoint
? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) ? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
: pointFrom(p[0], p[1]), : point(p[0], p[1]),
); );
nextSelectedIndices.push(indexCursor + 1); nextSelectedIndices.push(indexCursor + 1);
@@ -1198,7 +1194,7 @@ export class LinearElementEditor {
[ [
{ {
index: element.points.length - 1, index: element.points.length - 1,
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30), point: point(lastPoint[0] + 30, lastPoint[1] + 30),
}, },
], ],
elementsMap, elementsMap,
@@ -1239,9 +1235,7 @@ export class LinearElementEditor {
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => { const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
if (!pointIndices.includes(idx)) { if (!pointIndices.includes(idx)) {
acc.push( acc.push(
!acc.length !acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
? pointFrom(0, 0)
: pointFrom(p[0] - offsetX, p[1] - offsetY),
); );
} }
return acc; return acc;
@@ -1318,9 +1312,9 @@ export class LinearElementEditor {
const deltaY = const deltaY =
selectedPointData.point[1] - points[selectedPointData.index][1]; selectedPointData.point[1] - points[selectedPointData.index][1];
return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
} }
return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p; return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p;
}); });
LinearElementEditor._updatePoints( LinearElementEditor._updatePoints(
@@ -1374,8 +1368,8 @@ export class LinearElementEditor {
const origin = linearElementEditor.pointerDownState.origin!; const origin = linearElementEditor.pointerDownState.origin!;
const dist = pointDistance( const dist = pointDistance(
pointFrom(origin.x, origin.y), point(origin.x, origin.y),
pointFrom(pointerCoords.x, pointerCoords.y), point(pointerCoords.x, pointerCoords.y),
); );
if ( if (
!appState.editingLinearElement && !appState.editingLinearElement &&
@@ -1499,8 +1493,8 @@ export class LinearElementEditor {
const dX = prevCenterX - nextCenterX; const dX = prevCenterX - nextCenterX;
const dY = prevCenterY - nextCenterY; const dY = prevCenterY - nextCenterY;
const rotated = pointRotateRads( const rotated = pointRotateRads(
pointFrom(offsetX, offsetY), point(offsetX, offsetY),
pointFrom(dX, dY), point(dX, dY),
element.angle, element.angle,
); );
mutateElement(element, { mutateElement(element, {
@@ -1546,8 +1540,8 @@ export class LinearElementEditor {
); );
return pointRotateRads( return pointRotateRads(
pointFrom(width, height), point(width, height),
pointFrom(0, 0), point(0, 0),
-element.angle as Radians, -element.angle as Radians,
); );
} }
@@ -1617,36 +1611,36 @@ export class LinearElementEditor {
); );
const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextX2 = boundTextX1 + boundTextElement.width;
const boundTextY2 = boundTextY1 + boundTextElement.height; const boundTextY2 = boundTextY1 + boundTextElement.height;
const centerPoint = pointFrom(cx, cy); const centerPoint = point(cx, cy);
const topLeftRotatedPoint = pointRotateRads( const topLeftRotatedPoint = pointRotateRads(
pointFrom(x1, y1), point(x1, y1),
centerPoint, centerPoint,
element.angle, element.angle,
); );
const topRightRotatedPoint = pointRotateRads( const topRightRotatedPoint = pointRotateRads(
pointFrom(x2, y1), point(x2, y1),
centerPoint, centerPoint,
element.angle, element.angle,
); );
const counterRotateBoundTextTopLeft = pointRotateRads( const counterRotateBoundTextTopLeft = pointRotateRads(
pointFrom(boundTextX1, boundTextY1), point(boundTextX1, boundTextY1),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextTopRight = pointRotateRads( const counterRotateBoundTextTopRight = pointRotateRads(
pointFrom(boundTextX2, boundTextY1), point(boundTextX2, boundTextY1),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextBottomLeft = pointRotateRads( const counterRotateBoundTextBottomLeft = pointRotateRads(
pointFrom(boundTextX1, boundTextY2), point(boundTextX1, boundTextY2),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextBottomRight = pointRotateRads( const counterRotateBoundTextBottomRight = pointRotateRads(
pointFrom(boundTextX2, boundTextY2), point(boundTextX2, boundTextY2),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
+3 -26
View File
@@ -5,23 +5,12 @@ import { randomInteger } from "../random";
import { getUpdatedTimestamp } from "../utils"; import { getUpdatedTimestamp } from "../utils";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import { maybeGetSubtypeProps } from "./newElement";
import { getSubtypeMethods } from "./subtypes";
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit< export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
"id" | "version" | "versionNonce" | "updated" "id" | "version" | "versionNonce" | "updated"
>; >;
const cleanUpdates = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
): ElementUpdate<TElement> => {
const subtype = maybeGetSubtypeProps(element, element.type).subtype;
const map = getSubtypeMethods(subtype);
return map?.clean ? (map.clean(updates) as typeof updates) : updates;
};
// This function tracks updates of text elements for the purposes for collaboration. // This function tracks updates of text elements for the purposes for collaboration.
// The version is used to compare updates when more than one user is working in // The version is used to compare updates when more than one user is working in
// the same drawing. Note: this will trigger the component to update. Make sure you // the same drawing. Note: this will trigger the component to update. Make sure you
@@ -32,8 +21,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
informMutation = true, informMutation = true,
): TElement => { ): TElement => {
let didChange = false; let didChange = false;
let increment = false;
const oldUpdates = cleanUpdates(element, updates);
// casting to any because can't use `in` operator // casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
@@ -82,7 +69,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
} }
} }
if (!didChangePoints) { if (!didChangePoints) {
key in oldUpdates && (increment = true);
continue; continue;
} }
} }
@@ -90,7 +76,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
(element as any)[key] = value; (element as any)[key] = value;
didChange = true; didChange = true;
key in oldUpdates && (increment = true);
} }
} }
@@ -107,11 +92,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
ShapeCache.delete(element); ShapeCache.delete(element);
} }
if (increment) { element.version++;
element.version++; element.versionNonce = randomInteger();
element.versionNonce = randomInteger(); element.updated = getUpdatedTimestamp();
element.updated = getUpdatedTimestamp();
}
if (informMutation) { if (informMutation) {
Scene.getScene(element)?.triggerUpdate(); Scene.getScene(element)?.triggerUpdate();
@@ -127,8 +110,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
force = false, force = false,
): TElement => { ): TElement => {
let didChange = false; let didChange = false;
let increment = false;
const oldUpdates = cleanUpdates(element, updates);
for (const key in updates) { for (const key in updates) {
const value = (updates as any)[key]; const value = (updates as any)[key];
if (typeof value !== "undefined") { if (typeof value !== "undefined") {
@@ -140,7 +121,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
continue; continue;
} }
didChange = true; didChange = true;
key in oldUpdates && (increment = true);
} }
} }
@@ -148,9 +128,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
return element; return element;
} }
if (!increment) {
return { ...element, ...updates };
}
return { return {
...element, ...element,
...updates, ...updates,
@@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants";
import { isPrimitive } from "../utils"; import { isPrimitive } from "../utils";
import type { ExcalidrawLinearElement } from "./types"; import type { ExcalidrawLinearElement } from "./types";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom } from "../../math"; import { point } from "../../math";
const assertCloneObjects = (source: any, clone: any) => { const assertCloneObjects = (source: any, clone: any) => {
for (const key in clone) { for (const key in clone) {
@@ -38,7 +38,7 @@ describe("duplicating single elements", () => {
element.__proto__ = { hello: "world" }; element.__proto__ = { hello: "world" };
mutateElement(element, { mutateElement(element, {
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)], points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)],
}); });
const copy = duplicateElement(null, new Map(), element); const copy = duplicateElement(null, new Map(), element);
+27 -59
View File
@@ -19,7 +19,12 @@ import type {
ElementsMap, ElementsMap,
ExcalidrawArrowElement, ExcalidrawArrowElement,
} from "./types"; } from "./types";
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils"; import {
arrayToMap,
getFontString,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { bumpVersion, newElementWith } from "./mutateElement"; import { bumpVersion, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
@@ -27,9 +32,9 @@ import type { AppState } from "../types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
import { getResizedElementAbsoluteCoords } from "./bounds"; import { getResizedElementAbsoluteCoords } from "./bounds";
import { import {
measureTextElement, measureText,
normalizeText, normalizeText,
wrapTextElement, wrapText,
getBoundTextMaxWidth, getBoundTextMaxWidth,
} from "./textElement"; } from "./textElement";
import { import {
@@ -43,30 +48,6 @@ import {
import type { MarkOptional, Merge, Mutable } from "../utility-types"; import type { MarkOptional, Merge, Mutable } from "../utility-types";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { getSubtypeMethods, isValidSubtype } from "./subtypes";
export const maybeGetSubtypeProps = (
obj: {
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"];
},
type: ExcalidrawElement["type"],
) => {
const data: typeof obj = {};
if ("subtype" in obj) {
data.subtype = obj.subtype;
}
if ("customData" in obj) {
data.customData = obj.customData;
}
if ("subtype" in data && !isValidSubtype(data.subtype, type)) {
delete data.subtype;
}
if (!("subtype" in data) && "customData" in data) {
delete data.customData;
}
return data as typeof obj;
};
export type ElementConstructorOpts = MarkOptional< export type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -81,8 +62,6 @@ export type ElementConstructorOpts = MarkOptional<
| "version" | "version"
| "versionNonce" | "versionNonce"
| "link" | "link"
| "subtype"
| "customData"
| "strokeStyle" | "strokeStyle"
| "fillStyle" | "fillStyle"
| "strokeColor" | "strokeColor"
@@ -120,10 +99,8 @@ const _newElementBase = <T extends ExcalidrawElement>(
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => { ) => {
const { subtype, customData } = rest;
// assign type to guard against excess properties // assign type to guard against excess properties
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = { const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
...maybeGetSubtypeProps({ subtype, customData }, type),
id: rest.id || randomId(), id: rest.id || randomId(),
type, type,
x, x,
@@ -159,11 +136,8 @@ export const newElement = (
opts: { opts: {
type: ExcalidrawGenericElement["type"]; type: ExcalidrawGenericElement["type"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawGenericElement> => { ): NonDeleted<ExcalidrawGenericElement> =>
const map = getSubtypeMethods(opts?.subtype); _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
map?.clean && map.clean(opts);
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
};
export const newEmbeddableElement = ( export const newEmbeddableElement = (
opts: { opts: {
@@ -256,12 +230,10 @@ export const newTextElement = (
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE; const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getLineHeight(fontFamily); const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
const text = normalizeText(opts.text); const text = normalizeText(opts.text);
const metrics = measureTextElement( const metrics = measureText(
{ ...opts, fontSize, fontFamily, lineHeight }, text,
{ getFontString({ fontFamily, fontSize }),
text, lineHeight,
customData: opts.customData,
},
); );
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN; const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN; const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
@@ -305,9 +277,11 @@ const getAdjustedDimensions = (
width: number; width: number;
height: number; height: number;
} => { } => {
let { width: nextWidth, height: nextHeight } = measureTextElement(element, { let { width: nextWidth, height: nextHeight } = measureText(
text: nextText, nextText,
}); getFontString(element),
element.lineHeight,
);
// wrapped text // wrapped text
if (!element.autoResize) { if (!element.autoResize) {
@@ -323,7 +297,11 @@ const getAdjustedDimensions = (
!element.containerId && !element.containerId &&
element.autoResize element.autoResize
) { ) {
const prevMetrics = measureTextElement(element); const prevMetrics = measureText(
element.text,
getFontString(element),
element.lineHeight,
);
const offsets = getTextElementPositionOffsets(element, { const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width, width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height, height: nextHeight - prevMetrics.height,
@@ -426,14 +404,12 @@ export const refreshTextDimensions = (
return; return;
} }
if (container || !textElement.autoResize) { if (container || !textElement.autoResize) {
text = wrapTextElement( text = wrapText(
textElement, text,
getFontString(textElement),
container container
? getBoundTextMaxWidth(container, textElement) ? getBoundTextMaxWidth(container, textElement)
: textElement.width, : textElement.width,
{
text,
},
); );
} }
const dimensions = getAdjustedDimensions(textElement, elementsMap, text); const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
@@ -448,8 +424,6 @@ export const newFreeDrawElement = (
pressures?: ExcalidrawFreeDrawElement["pressures"]; pressures?: ExcalidrawFreeDrawElement["pressures"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawFreeDrawElement> => { ): NonDeleted<ExcalidrawFreeDrawElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return { return {
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts), ..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
points: opts.points || [], points: opts.points || [],
@@ -465,8 +439,6 @@ export const newLinearElement = (
points?: ExcalidrawLinearElement["points"]; points?: ExcalidrawLinearElement["points"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawLinearElement> => { ): NonDeleted<ExcalidrawLinearElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return { return {
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts), ..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
points: opts.points || [], points: opts.points || [],
@@ -487,8 +459,6 @@ export const newArrowElement = (
elbowed?: boolean; elbowed?: boolean;
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawArrowElement> => { ): NonDeleted<ExcalidrawArrowElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return { return {
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts), ..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
points: opts.points || [], points: opts.points || [],
@@ -509,8 +479,6 @@ export const newImageElement = (
scale?: ExcalidrawImageElement["scale"]; scale?: ExcalidrawImageElement["scale"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawImageElement> => { ): NonDeleted<ExcalidrawImageElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return { return {
..._newElementBase<ExcalidrawImageElement>("image", opts), ..._newElementBase<ExcalidrawImageElement>("image", opts),
// in the future we'll support changing stroke color for some SVG elements, // in the future we'll support changing stroke color for some SVG elements,
+42 -54
View File
@@ -58,7 +58,7 @@ import type { GlobalPoint } from "../../math";
import { import {
pointCenter, pointCenter,
normalizeRadians, normalizeRadians,
pointFrom, point,
pointFromPair, pointFromPair,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
@@ -240,8 +240,8 @@ const resizeSingleTextElement = (
); );
// rotation pointer with reverse angle // rotation pointer with reverse angle
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
pointFrom(pointerX, pointerY), point(pointerX, pointerY),
pointFrom(cx, cy), point(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
let scaleX = 0; let scaleX = 0;
@@ -276,23 +276,23 @@ const resizeSingleTextElement = (
const startBottomRight = [x2, y2]; const startBottomRight = [x2, y2];
const startCenter = [cx, cy]; const startCenter = [cx, cy];
let newTopLeft = pointFrom<GlobalPoint>(x1, y1); let newTopLeft = point<GlobalPoint>(x1, y1);
if (["n", "w", "nw"].includes(transformHandleType)) { if (["n", "w", "nw"].includes(transformHandleType)) {
newTopLeft = pointFrom<GlobalPoint>( newTopLeft = point<GlobalPoint>(
startBottomRight[0] - Math.abs(nextWidth), startBottomRight[0] - Math.abs(nextWidth),
startBottomRight[1] - Math.abs(nextHeight), startBottomRight[1] - Math.abs(nextHeight),
); );
} }
if (transformHandleType === "ne") { if (transformHandleType === "ne") {
const bottomLeft = [startTopLeft[0], startBottomRight[1]]; const bottomLeft = [startTopLeft[0], startBottomRight[1]];
newTopLeft = pointFrom<GlobalPoint>( newTopLeft = point<GlobalPoint>(
bottomLeft[0], bottomLeft[0],
bottomLeft[1] - Math.abs(nextHeight), bottomLeft[1] - Math.abs(nextHeight),
); );
} }
if (transformHandleType === "sw") { if (transformHandleType === "sw") {
const topRight = [startBottomRight[0], startTopLeft[1]]; const topRight = [startBottomRight[0], startTopLeft[1]];
newTopLeft = pointFrom<GlobalPoint>( newTopLeft = point<GlobalPoint>(
topRight[0] - Math.abs(nextWidth), topRight[0] - Math.abs(nextWidth),
topRight[1], topRight[1],
); );
@@ -311,20 +311,12 @@ const resizeSingleTextElement = (
} }
const angle = element.angle; const angle = element.angle;
const rotatedTopLeft = pointRotateRads( const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
newTopLeft, const newCenter = point<GlobalPoint>(
pointFrom(cx, cy),
angle,
);
const newCenter = pointFrom<GlobalPoint>(
newTopLeft[0] + Math.abs(nextWidth) / 2, newTopLeft[0] + Math.abs(nextWidth) / 2,
newTopLeft[1] + Math.abs(nextHeight) / 2, newTopLeft[1] + Math.abs(nextHeight) / 2,
); );
const rotatedNewCenter = pointRotateRads( const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
newCenter,
pointFrom(cx, cy),
angle,
);
newTopLeft = pointRotateRads( newTopLeft = pointRotateRads(
rotatedTopLeft, rotatedTopLeft,
rotatedNewCenter, rotatedNewCenter,
@@ -349,12 +341,12 @@ const resizeSingleTextElement = (
stateAtResizeStart.height, stateAtResizeStart.height,
true, true,
); );
const startTopLeft = pointFrom<GlobalPoint>(x1, y1); const startTopLeft = point<GlobalPoint>(x1, y1);
const startBottomRight = pointFrom<GlobalPoint>(x2, y2); const startBottomRight = point<GlobalPoint>(x2, y2);
const startCenter = pointCenter(startTopLeft, startBottomRight); const startCenter = pointCenter(startTopLeft, startBottomRight);
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY), point(pointerX, pointerY),
startCenter, startCenter,
-stateAtResizeStart.angle as Radians, -stateAtResizeStart.angle as Radians,
); );
@@ -427,7 +419,7 @@ const resizeSingleTextElement = (
startCenter, startCenter,
angle, angle,
); );
const newCenter = pointFrom( const newCenter = point(
newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
); );
@@ -469,13 +461,13 @@ export const resizeSingleElement = (
stateAtResizeStart.height, stateAtResizeStart.height,
true, true,
); );
const startTopLeft = pointFrom(x1, y1); const startTopLeft = point(x1, y1);
const startBottomRight = pointFrom(x2, y2); const startBottomRight = point(x2, y2);
const startCenter = pointCenter(startTopLeft, startBottomRight); const startCenter = pointCenter(startTopLeft, startBottomRight);
// Calculate new dimensions based on cursor position // Calculate new dimensions based on cursor position
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY), point(pointerX, pointerY),
startCenter, startCenter,
-stateAtResizeStart.angle as Radians, -stateAtResizeStart.angle as Radians,
); );
@@ -656,7 +648,7 @@ export const resizeSingleElement = (
startCenter, startCenter,
angle, angle,
); );
const newCenter = pointFrom( const newCenter = point(
newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
); );
@@ -825,20 +817,20 @@ export const resizeMultipleElements = (
const direction = transformHandleType; const direction = transformHandleType;
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = { const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
ne: pointFrom(minX, maxY), ne: point(minX, maxY),
se: pointFrom(minX, minY), se: point(minX, minY),
sw: pointFrom(maxX, minY), sw: point(maxX, minY),
nw: pointFrom(maxX, maxY), nw: point(maxX, maxY),
e: pointFrom(minX, minY + height / 2), e: point(minX, minY + height / 2),
w: pointFrom(maxX, minY + height / 2), w: point(maxX, minY + height / 2),
n: pointFrom(minX + width / 2, maxY), n: point(minX + width / 2, maxY),
s: pointFrom(minX + width / 2, minY), s: point(minX + width / 2, minY),
}; };
// anchor point must be on the opposite side of the dragged selection handle // anchor point must be on the opposite side of the dragged selection handle
// or be the center of the selection if shouldResizeFromCenter // or be the center of the selection if shouldResizeFromCenter
const [anchorX, anchorY] = shouldResizeFromCenter const [anchorX, anchorY] = shouldResizeFromCenter
? pointFrom(midX, midY) ? point(midX, midY)
: anchorsMap[direction]; : anchorsMap[direction];
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1; const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
@@ -1052,8 +1044,8 @@ const rotateMultipleElements = (
const origAngle = const origAngle =
originalElements.get(element.id)?.angle ?? element.angle; originalElements.get(element.id)?.angle ?? element.angle;
const [rotatedCX, rotatedCY] = pointRotateRads( const [rotatedCX, rotatedCY] = pointRotateRads(
pointFrom(cx, cy), point(cx, cy),
pointFrom(centerX, centerY), point(centerX, centerY),
(centerAngle + origAngle - element.angle) as Radians, (centerAngle + origAngle - element.angle) as Radians,
); );
@@ -1109,44 +1101,40 @@ export const getResizeOffsetXY = (
const angle = ( const angle = (
selectedElements.length === 1 ? selectedElements[0].angle : 0 selectedElements.length === 1 ? selectedElements[0].angle : 0
) as Radians; ) as Radians;
[x, y] = pointRotateRads( [x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
pointFrom(x, y),
pointFrom(cx, cy),
-angle as Radians,
);
switch (transformHandleType) { switch (transformHandleType) {
case "n": case "n":
return pointRotateRads( return pointRotateRads(
pointFrom(x - (x1 + x2) / 2, y - y1), point(x - (x1 + x2) / 2, y - y1),
pointFrom(0, 0), point(0, 0),
angle, angle,
); );
case "s": case "s":
return pointRotateRads( return pointRotateRads(
pointFrom(x - (x1 + x2) / 2, y - y2), point(x - (x1 + x2) / 2, y - y2),
pointFrom(0, 0), point(0, 0),
angle, angle,
); );
case "w": case "w":
return pointRotateRads( return pointRotateRads(
pointFrom(x - x1, y - (y1 + y2) / 2), point(x - x1, y - (y1 + y2) / 2),
pointFrom(0, 0), point(0, 0),
angle, angle,
); );
case "e": case "e":
return pointRotateRads( return pointRotateRads(
pointFrom(x - x2, y - (y1 + y2) / 2), point(x - x2, y - (y1 + y2) / 2),
pointFrom(0, 0), point(0, 0),
angle, angle,
); );
case "nw": case "nw":
return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle); return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle);
case "ne": case "ne":
return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle); return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
case "sw": case "sw":
return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle); return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle);
case "se": case "se":
return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle); return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle);
default: default:
return [0, 0]; return [0, 0];
} }
+13 -17
View File
@@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants";
import { isLinearElement } from "./typeChecks"; import { isLinearElement } from "./typeChecks";
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
import { import {
pointFrom, point,
pointOnLineSegment, pointOnLineSegment,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
@@ -92,20 +92,16 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
if (!(isLinearElement(element) && element.points.length <= 2)) { if (!(isLinearElement(element) && element.points.length <= 2)) {
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders( const sides = getSelectionBorders(
pointFrom(x1 - SPACING, y1 - SPACING), point(x1 - SPACING, y1 - SPACING),
pointFrom(x2 + SPACING, y2 + SPACING), point(x2 + SPACING, y2 + SPACING),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
for (const [dir, side] of Object.entries(sides)) { for (const [dir, side] of Object.entries(sides)) {
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment( pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
pointFrom(x, y),
side as LineSegment<Point>,
SPACING,
)
) { ) {
return dir as TransformHandleType; return dir as TransformHandleType;
} }
@@ -182,9 +178,9 @@ export const getTransformHandleTypeFromCoords = <
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders( const sides = getSelectionBorders(
pointFrom(x1 - SPACING, y1 - SPACING), point(x1 - SPACING, y1 - SPACING),
pointFrom(x2 + SPACING, y2 + SPACING), point(x2 + SPACING, y2 + SPACING),
pointFrom(cx, cy), point(cx, cy),
0 as Radians, 0 as Radians,
); );
@@ -192,7 +188,7 @@ export const getTransformHandleTypeFromCoords = <
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment( pointOnLineSegment(
pointFrom(scenePointerX, scenePointerY), point(scenePointerX, scenePointerY),
side as LineSegment<Point>, side as LineSegment<Point>,
SPACING, SPACING,
) )
@@ -269,10 +265,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
center: Point, center: Point,
angle: Radians, angle: Radians,
) => { ) => {
const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle); const topLeft = pointRotateRads(point(x1, y1), center, angle);
const topRight = pointRotateRads(pointFrom(x2, y1), center, angle); const topRight = pointRotateRads(point(x2, y1), center, angle);
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle); const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle); const bottomRight = pointRotateRads(point(x2, y2), center, angle);
return { return {
n: [topLeft, topRight], n: [topLeft, topRight],
+5 -5
View File
@@ -17,7 +17,7 @@ import type {
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
import { ARROW_TYPE } from "../constants"; import { ARROW_TYPE } from "../constants";
import { pointFrom } from "../../math"; import { point } from "../../math";
const { h } = window; const { h } = window;
@@ -32,8 +32,8 @@ describe("elbow arrow routing", () => {
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow); scene.insertElement(arrow);
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [ mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
pointFrom(-45 - arrow.x, -100.1 - arrow.y), point(-45 - arrow.x, -100.1 - arrow.y),
pointFrom(45 - arrow.x, 99.9 - arrow.y), point(45 - arrow.x, 99.9 - arrow.y),
]); ]);
expect(arrow.points).toEqual([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
@@ -69,7 +69,7 @@ describe("elbow arrow routing", () => {
y: -100.1, y: -100.1,
width: 90, width: 90,
height: 200, height: 200,
points: [pointFrom(0, 0), pointFrom(90, 200)], points: [point(0, 0), point(90, 200)],
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(rectangle1); scene.insertElement(rectangle1);
scene.insertElement(rectangle2); scene.insertElement(rectangle2);
@@ -81,7 +81,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);
mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]); mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]);
expect(arrow.points).toEqual([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
+5 -5
View File
@@ -1,6 +1,6 @@
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { import {
pointFrom, point,
pointScaleFromOrigin, pointScaleFromOrigin,
pointTranslate, pointTranslate,
vector, vector,
@@ -743,13 +743,13 @@ const getDonglePosition = (
): GlobalPoint => { ): GlobalPoint => {
switch (heading) { switch (heading) {
case HEADING_UP: case HEADING_UP:
return pointFrom(p[0], bounds[1]); return point(p[0], bounds[1]);
case HEADING_RIGHT: case HEADING_RIGHT:
return pointFrom(bounds[2], p[1]); return point(bounds[2], p[1]);
case HEADING_DOWN: case HEADING_DOWN:
return pointFrom(p[0], bounds[3]); return point(p[0], bounds[3]);
} }
return pointFrom(bounds[0], p[1]); return point(bounds[0], p[1]);
}; };
const estimateSegmentCount = ( const estimateSegmentCount = (
@@ -1,541 +0,0 @@
import { useEffect } from "react";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawTextElement,
NonDeleted,
} from "../types";
import { getNonDeletedElements } from "../";
import { getSelectedElements } from "../../scene";
import type { AppState, ExcalidrawImperativeAPI, ToolType } from "../../types";
import type { LangLdr } from "../../i18n";
import { registerCustomLangData } from "../../i18n";
import type {
Action,
ActionName,
ActionPredicateFn,
CustomActionName,
} from "../../actions/types";
import { makeCustomActionName } from "../../actions/types";
import { registerCustomShortcuts } from "../../actions/shortcuts";
import { register } from "../../actions/register";
import { hasBoundTextElement, isTextElement } from "../typeChecks";
import {
getBoundTextElement,
getContainerElement,
redrawTextBoundingBox,
} from "../textElement";
import { ShapeCache } from "../../scene/ShapeCache";
import Scene from "../../scene/Scene";
// Use "let" instead of "const" so we can dynamically add subtypes
let subtypeNames: readonly Subtype[] = [];
let parentTypeMap: readonly {
subtype: Subtype;
parentType: ExcalidrawElement["type"];
}[] = [];
let subtypeActionMap: readonly {
subtype: Subtype;
actions: readonly ActionName[];
}[] = [];
let disabledActionMap: readonly {
subtype: Subtype;
actions: readonly DisabledActionName[];
}[] = [];
let alwaysEnabledMap: readonly {
subtype: Subtype;
actions: readonly SubtypeActionName[];
}[] = [];
export type SubtypeRecord = Readonly<{
subtype: Subtype;
parents: readonly (ExcalidrawElement["type"] & ToolType)[];
actionNames?: readonly SubtypeActionName[];
disabledNames?: readonly DisabledActionName[];
shortcutMap?: Record<string, string[]>;
alwaysEnabledNames?: readonly SubtypeActionName[];
}>;
// Subtype Names
export type Subtype = Required<ExcalidrawElement>["subtype"];
export const getSubtypeNames = (): readonly Subtype[] => {
return subtypeNames;
};
export const isValidSubtype = (s: any, t: any): s is Subtype =>
parentTypeMap.find(
(val) => (val.subtype as any) === s && (val.parentType as any) === t,
) !== undefined;
const isSubtypeName = (s: any): s is Subtype => subtypeNames.includes(s);
// Subtype Actions
// Used for context menus in the shape chooser
export const hasAlwaysEnabledActions = (s: any): boolean => {
if (!isSubtypeName(s)) {
return false;
}
return alwaysEnabledMap.some((value) => value.subtype === s);
};
type SubtypeActionName = string;
const isSubtypeActionName = (s: any): s is SubtypeActionName =>
subtypeActionMap.some((val) => val.actions.includes(s));
const addSubtypeAction = (action: Action) => {
if (isSubtypeActionName(action.name) || isSubtypeName(action.name)) {
register(action);
}
};
// Standard actions disabled by subtypes
type DisabledActionName = ActionName;
const isDisabledActionName = (s: any): s is DisabledActionName =>
disabledActionMap.some((val) => val.actions.includes(s));
// Is the `actionName` one of the subtype actions for `subtype`
// (if `isAdded` is true) or one of the standard actions disabled
// by `subtype` (if `isAdded` is false)?
const isForSubtype = (
subtype: ExcalidrawElement["subtype"],
actionName: ActionName,
isAdded: boolean,
) => {
const actions = isAdded ? subtypeActionMap : disabledActionMap;
const map = actions.find((value) => value.subtype === subtype);
if (map) {
return map.actions.includes(actionName);
}
return false;
};
export const isSubtypeAction: ActionPredicateFn = function (action) {
return isSubtypeActionName(action.name) && !isSubtypeName(action.name);
};
export const subtypeActionPredicate: ActionPredicateFn = function (
action,
elements,
appState,
app,
) {
// We always enable subtype actions. Also let through standard actions
// which no subtypes might have disabled.
if (
isSubtypeName(action.name) ||
(!isSubtypeActionName(action.name) && !isDisabledActionName(action.name))
) {
return true;
}
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const chosen = appState.editingTextElement
? [appState.editingTextElement, ...selectedElements]
: selectedElements;
// Now handle actions added by subtypes
if (isSubtypeActionName(action.name)) {
// Has any ExcalidrawElement enabled this actionName through having
// its subtype?
return (
chosen.some((el) => {
const e = hasBoundTextElement(el)
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
: el;
return isForSubtype(e.subtype, action.name, true);
}) ||
// Or has any active subtype enabled this actionName?
(appState.activeSubtypes !== undefined &&
appState.activeSubtypes?.some((subtype) => {
if (!isValidSubtype(subtype, appState.activeTool.type)) {
return false;
}
return isForSubtype(subtype, action.name, true);
})) ||
alwaysEnabledMap.some((value) => {
return value.actions.includes(action.name);
})
);
}
// Now handle standard actions disabled by subtypes
if (isDisabledActionName(action.name)) {
return (
// Has every ExcalidrawElement not disabled this actionName?
(chosen.every((el) => {
const e = hasBoundTextElement(el)
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
: el;
return !isForSubtype(e.subtype, action.name, false);
}) &&
// And has every active subtype not disabled this actionName?
(appState.activeSubtypes === undefined ||
appState.activeSubtypes?.every((subtype) => {
if (!isValidSubtype(subtype, appState.activeTool.type)) {
return true;
}
return !isForSubtype(subtype, action.name, false);
}))) ||
// Or can we find an ExcalidrawElement without a valid subtype
// which would disable this action if it had a valid subtype?
chosen.some((el) => {
const e = hasBoundTextElement(el)
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
: el;
return parentTypeMap.some(
(value) =>
value.parentType === e.type &&
!isValidSubtype(e.subtype, e.type) &&
isForSubtype(value.subtype, action.name, false),
);
}) ||
chosen.some((el) => {
const e = hasBoundTextElement(el)
? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())!
: el;
return (
// Would the subtype of e by inself disable this action?
isForSubtype(e.subtype, action.name, false) &&
// Can we find an ExcalidrawElement which could have the same subtype
// as e but whose subtype does not disable this action?
chosen.some((el) => {
const e2 = hasBoundTextElement(el)
? getBoundTextElement(
el,
app.scene.getElementsMapIncludingDeleted(),
)!
: el;
return (
// Does e have a valid subtype whose parent types include the
// type of e2, and does the subtype of e2 not disable this action?
parentTypeMap
.filter((val) => val.subtype === e.subtype)
.some((val) => val.parentType === e2.type) &&
!isForSubtype(e2.subtype, action.name, false)
);
})
);
})
);
}
// Shouldn't happen
return true;
};
// Are any of the parent types of `subtype` shared by any subtype
// in the array?
export const subtypeCollides = (subtype: Subtype, subtypeArray: Subtype[]) => {
const subtypeParents = parentTypeMap
.filter((value) => value.subtype === subtype)
.map((value) => value.parentType);
const subtypeArrayParents = subtypeArray.flatMap((s) =>
parentTypeMap
.filter((value) => value.subtype === s)
.map((value) => value.parentType),
);
return subtypeParents.some((t) => subtypeArrayParents.includes(t));
};
// Subtype Methods
export type SubtypeMethods = {
clean: (
updates: Omit<
Partial<ExcalidrawElement>,
"id" | "version" | "versionNonce"
>,
) => Omit<Partial<ExcalidrawElement>, "id" | "version" | "versionNonce">;
getEditorStyle: (element: ExcalidrawTextElement) => Record<string, any>;
ensureLoaded: (callback?: () => void) => Promise<void>;
measureText: (
element: Pick<
ExcalidrawTextElement,
| "subtype"
| "customData"
| "fontSize"
| "fontFamily"
| "text"
| "lineHeight"
>,
next?: {
fontSize?: number;
text?: string;
customData?: ExcalidrawElement["customData"];
},
) => { width: number; height: number };
render: (
element: NonDeleted<ExcalidrawElement>,
elementsMap: ElementsMap,
context: CanvasRenderingContext2D,
) => void;
renderSvg: (
svgRoot: SVGElement,
addToRoot: (node: SVGElement, element: ExcalidrawElement) => void,
element: NonDeleted<ExcalidrawElement>,
elementsMap: ElementsMap,
opt?: { offsetX?: number; offsetY?: number },
) => void;
wrapText: (
element: Pick<
ExcalidrawTextElement,
| "subtype"
| "customData"
| "fontSize"
| "fontFamily"
| "originalText"
| "lineHeight"
>,
containerWidth: number,
next?: {
fontSize?: number;
text?: string;
customData?: ExcalidrawElement["customData"];
},
) => string;
};
type MethodMap = { subtype: Subtype; methods: Partial<SubtypeMethods> };
const methodMaps = [] as Array<MethodMap>;
// Use `getSubtypeMethods` to call subtype-specialized methods, like `render`.
export const getSubtypeMethods = (
subtype: Subtype | undefined,
): Partial<SubtypeMethods> | undefined => {
const map = methodMaps.find((method) => method.subtype === subtype);
return map?.methods;
};
export const addSubtypeMethods = (
subtype: Subtype,
methods: Partial<SubtypeMethods>,
) => {
if (!methodMaps.find((method) => method.subtype === subtype)) {
methodMaps.push({ subtype, methods });
}
};
// For a given `ExcalidrawElement` type, return the active subtype
// and associated customData (if any) from the AppState. Assume
// only one subtype is active for a given `ExcalidrawElement` type
// at any given time.
export const selectSubtype = (
appState: {
activeSubtypes?: AppState["activeSubtypes"];
customData?: AppState["customData"];
},
type: ExcalidrawElement["type"],
): {
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"];
} => {
if (appState.activeSubtypes === undefined) {
return {};
}
const subtype = appState.activeSubtypes.find((subtype) =>
isValidSubtype(subtype, type),
);
if (subtype === undefined) {
return {};
}
if (appState.customData === undefined || !(subtype in appState.customData)) {
return { subtype };
}
const customData = appState.customData[subtype];
return { subtype, customData };
};
// Callback to re-render subtyped `ExcalidrawElement`s after completing
// async loading of the subtype.
export type SubtypeLoadedCb = (hasSubtype: SubtypeCheckFn) => void;
export type SubtypeCheckFn = (element: ExcalidrawElement) => boolean;
// Functions to prepare subtypes for use
export type SubtypePrepFn = (
addSubtypeAction: (action: Action) => void,
addLangData: (fallbackLangData: {}, setLanguageAux: LangLdr) => void,
onSubtypeLoaded?: SubtypeLoadedCb,
) => {
actions: Action[];
methods: Partial<SubtypeMethods>;
};
// This is the main method to set up the subtype. The optional
// `onSubtypeLoaded` callback may be used to re-render subtyped
// `ExcalidrawElement`s after the subtype has finished async loading.
// See the MathJax extension in `@excalidraw/extensions` for example.
export const prepareSubtype = (
record: SubtypeRecord,
subtypePrepFn: SubtypePrepFn,
onSubtypeLoaded?: SubtypeLoadedCb,
): { actions: readonly Action[] | null; methods: Partial<SubtypeMethods> } => {
const map = getSubtypeMethods(record.subtype);
if (map) {
return { actions: null, methods: map };
}
// Check for undefined/null subtypes and parentTypes
if (
record.subtype === undefined ||
record.subtype === "" ||
record.parents === undefined ||
record.parents.length === 0
) {
return { actions: null, methods: {} };
}
// Register the types
const subtype = record.subtype;
subtypeNames = [...subtypeNames, subtype];
record.parents.forEach((parentType) => {
parentTypeMap = [...parentTypeMap, { subtype, parentType }];
});
if (record.actionNames) {
subtypeActionMap = [
...subtypeActionMap,
{
subtype,
actions: record.actionNames.map((actionName) =>
makeCustomActionName(actionName),
),
},
];
}
if (record.disabledNames) {
disabledActionMap = [
...disabledActionMap,
{ subtype, actions: record.disabledNames },
];
}
if (record.alwaysEnabledNames) {
alwaysEnabledMap = [
...alwaysEnabledMap,
{
subtype,
actions: record.alwaysEnabledNames.map((actionName) =>
makeCustomActionName(actionName),
),
},
];
}
const customShortcutMap = record.shortcutMap;
if (customShortcutMap) {
const shortcutMap: Record<CustomActionName, string[]> = {};
for (const key in customShortcutMap) {
shortcutMap[makeCustomActionName(key)] = customShortcutMap[key];
}
registerCustomShortcuts(shortcutMap);
}
// Prepare the subtype
const { actions, methods } = subtypePrepFn(
addSubtypeAction,
registerCustomLangData,
onSubtypeLoaded,
);
// Register the subtype's methods
addSubtypeMethods(record.subtype, methods);
return { actions, methods };
};
// Ensure all subtypes are loaded before continuing, eg to
// render SVG previews of new charts. Chart-relevant subtypes
// include math equations in titles or non hand-drawn line styles.
export const ensureSubtypesLoadedForElements = async (
elements: readonly ExcalidrawElement[],
callback?: () => void,
) => {
// Only ensure the loading of subtypes which are actually needed.
// We don't want to be held up by eg downloading the MathJax SVG fonts
// if we don't actually need them yet.
const subtypesUsed = [] as Subtype[];
elements.forEach((el) => {
if (
"subtype" in el &&
isValidSubtype(el.subtype, el.type) &&
!subtypesUsed.includes(el.subtype)
) {
subtypesUsed.push(el.subtype);
}
});
await ensureSubtypesLoaded(subtypesUsed, callback);
};
export const ensureSubtypesLoaded = async (
subtypes: Subtype[],
callback?: () => void,
) => {
// Use a for loop so we can do `await map.ensureLoaded()`
for (let i = 0; i < subtypes.length; i++) {
const subtype = subtypes[i];
// Should be defined if prepareSubtype() has run
const map = getSubtypeMethods(subtype);
if (map?.ensureLoaded) {
await map.ensureLoaded();
}
}
if (callback) {
callback();
}
};
// Call this method after finishing any async loading for
// subtypes of ExcalidrawElement if the newly loaded code
// would change the rendering.
export const checkRefreshOnSubtypeLoad = (
hasSubtype: SubtypeCheckFn,
elements: readonly ExcalidrawElement[],
) => {
const elementsMap = new Map() as ElementsMap;
for (const element of elements) {
if (!element.isDeleted) {
elementsMap.set(element.id, element);
}
}
let refreshNeeded = false;
const scenes: Scene[] = [];
getNonDeletedElements(elements).forEach((element) => {
// If the element is of the subtype that was just
// registered, update the element's dimensions, mark the
// element for a re-render, and indicate the scene needs a refresh.
if (hasSubtype(element)) {
ShapeCache.delete(element);
if (isTextElement(element)) {
redrawTextBoundingBox(
element,
getContainerElement(element, elementsMap),
elementsMap,
false,
);
}
refreshNeeded = true;
const scene = Scene.getScene(element);
if (scene && !scenes.includes(scene)) {
// Store in case we have multiple scenes
scenes.push(scene);
}
}
});
// Only inform each scene once
scenes.forEach((scene) => scene.triggerUpdate());
return refreshNeeded;
};
export const useSubtype = (
api: ExcalidrawImperativeAPI | null,
record: SubtypeRecord,
subtypePrepFn: SubtypePrepFn,
) => {
useEffect(() => {
if (api) {
const prep = api.addSubtype(record, subtypePrepFn);
if (prep) {
addSubtypeMethods(record.subtype, prep.methods);
if (prep.actions) {
prep.actions.forEach((action) => api.registerAction(action));
}
}
}
}, [api, record, subtypePrepFn]);
};
@@ -1,13 +0,0 @@
import type { Theme } from "../../../element/types";
import { createIcon, iconFillColor } from "../../../components/icons";
// We inline font-awesome icons in order to save on js size rather than including the font awesome react library
export const mathSubtypeIcon = ({ theme }: { theme: Theme }) =>
createIcon(
<path
fill={iconFillColor(theme)}
// fa-square-root-variable-solid
d="M289 24.2C292.5 10 305.3 0 320 0H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H345L239 487.8c-3.2 13-14.2 22.6-27.6 24s-26.1-5.5-32.1-17.5L76.2 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H96c12.1 0 23.2 6.8 28.6 17.7l73.3 146.6L289 24.2zM393.4 233.4c12.5-12.5 32.8-12.5 45.3 0L480 274.7l41.4-41.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L525.3 320l41.4 41.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L480 365.3l-41.4 41.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L434.7 320l-41.4-41.4c-12.5-12.5-12.5-32.8 0-45.3z"
/>,
{ width: 576, height: 512, mirror: true, strokeWidth: 1.25 },
);
File diff suppressed because it is too large Load Diff
@@ -1,15 +0,0 @@
import type { ExcalidrawImperativeAPI } from "../../../types";
import { useSubtype } from "../";
import { getMathSubtypeRecord } from "./types";
import { prepareMathSubtype } from "./implementation";
declare global {
module SREfeature {
function custom(locale: string): Promise<string>;
}
}
// The main hook to use the MathJax subtype
export const useMathSubtype = (api: ExcalidrawImperativeAPI | null) => {
useSubtype(api, getMathSubtypeRecord(), prepareMathSubtype);
};
@@ -1,15 +0,0 @@
{
"labels": {
"changeMathOnly": "Math display",
"mathOnlyTrue": "Math only",
"mathOnlyFalse": "Mixed text",
"resetUseTex": "Reset math input type",
"useTexTrueActive": "✔ Standard input",
"useTexTrueInactive": "Standard input",
"useTexFalseActive": "✔ Simplified input",
"useTexFalseInactive": "Simplified input"
},
"toolBar": {
"math": "Math"
}
}
@@ -1,76 +0,0 @@
import { vi } from "vitest";
import { render } from "../../../../tests/test-utils";
import { API } from "../../../../tests/helpers/api";
import { Excalidraw } from "../../../../index";
import { measureTextElement } from "../../../textElement";
import { ensureSubtypesLoaded } from "../../";
import { getMathSubtypeRecord } from "../types";
import { prepareMathSubtype } from "../implementation";
describe("mathjax loaded", () => {
beforeEach(async () => {
await render(<Excalidraw />);
API.addSubtype(getMathSubtypeRecord(), prepareMathSubtype);
await ensureSubtypesLoaded(["math"]);
});
it("text-only measurements match", async () => {
const text = "A quick brown fox jumps over the lazy dog.";
const elements = [
API.createElement({ type: "text", id: "A", text, subtype: "math" }),
API.createElement({ type: "text", id: "B", text }),
];
const metrics1 = measureTextElement(elements[0]);
const metrics2 = measureTextElement(elements[1]);
expect(metrics1).toStrictEqual(metrics2);
});
it("minimum height remains", async () => {
const elements = [
API.createElement({ type: "text", id: "A", text: "a" }),
API.createElement({
type: "text",
id: "B",
text: "\\(\\alpha\\)",
subtype: "math",
customData: { useTex: true },
}),
API.createElement({
type: "text",
id: "C",
text: "`beta`",
subtype: "math",
customData: { useTex: false },
}),
];
const height = measureTextElement(elements[0]).height;
const height1 = measureTextElement(elements[1]).height;
const height2 = measureTextElement(elements[2]).height;
expect(height).toEqual(height1);
expect(height).toEqual(height2);
});
it("converts math to svgs", async () => {
const svgDim = 42;
vi.spyOn(SVGElement.prototype, "getBoundingClientRect").mockImplementation(
() => new DOMRect(0, 0, svgDim, svgDim),
);
const elements = [];
const type = "text";
const subtype = "math";
let text = "Math ";
elements.push(API.createElement({ type, text }));
text = "Math \\(\\alpha\\)";
elements.push(
API.createElement({ type, subtype, text, customData: { useTex: true } }),
);
text = "Math `beta`";
elements.push(
API.createElement({ type, subtype, text, customData: { useTex: false } }),
);
const metrics = {
width: measureTextElement(elements[0]).width + svgDim,
height: svgDim,
};
expect(measureTextElement(elements[1])).toStrictEqual(metrics);
expect(measureTextElement(elements[2])).toStrictEqual(metrics);
});
});
@@ -1,17 +0,0 @@
import { getShortcutKey } from "../../../utils";
import type { SubtypeRecord } from "../";
// Exports
export const getMathSubtypeRecord = () => mathSubtype;
// Use `getMathSubtype` so we don't have to export this
const mathSubtype: SubtypeRecord = {
subtype: "math",
parents: ["text"],
actionNames: ["useTexTrue", "useTexFalse", "resetUseTex", "changeMathOnly"],
disabledNames: ["changeFontFamily"],
shortcutMap: {
resetUseTex: [getShortcutKey("Shift+R")],
},
alwaysEnabledNames: ["useTexTrue", "useTexFalse"],
};
+20 -40
View File
@@ -1,5 +1,3 @@
import type { SubtypeMethods } from "./subtypes";
import { getSubtypeMethods } from "./subtypes";
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils"; import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
import type { import type {
ElementsMap, ElementsMap,
@@ -32,30 +30,6 @@ import {
} from "./containerCache"; } from "./containerCache";
import type { ExtractSetType } from "../utility-types"; import type { ExtractSetType } from "../utility-types";
export const measureTextElement = function (element, next) {
const map = getSubtypeMethods(element.subtype);
if (map?.measureText) {
return map.measureText(element, next);
}
const fontSize = next?.fontSize ?? element.fontSize;
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
const text = next?.text ?? element.text;
return measureText(text, font, element.lineHeight);
} as SubtypeMethods["measureText"];
export const wrapTextElement = function (element, containerWidth, next) {
const map = getSubtypeMethods(element.subtype);
if (map?.wrapText) {
return map.wrapText(element, containerWidth, next);
}
const fontSize = next?.fontSize ?? element.fontSize;
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
const text = next?.text ?? element.originalText;
return wrapText(text, font, containerWidth);
} as SubtypeMethods["wrapText"];
export const normalizeText = (text: string) => { export const normalizeText = (text: string) => {
return ( return (
normalizeEOL(text) normalizeEOL(text)
@@ -90,12 +64,18 @@ export const redrawTextBoundingBox = (
maxWidth = container maxWidth = container
? getBoundTextMaxWidth(container, textElement) ? getBoundTextMaxWidth(container, textElement)
: textElement.width; : textElement.width;
boundTextUpdates.text = wrapTextElement(textElement, maxWidth); boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
} }
const metrics = measureTextElement(textElement, { const metrics = measureText(
text: boundTextUpdates.text, boundTextUpdates.text,
}); getFontString(textElement),
textElement.lineHeight,
);
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true) // Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
if (textElement.autoResize) { if (textElement.autoResize) {
@@ -103,14 +83,6 @@ export const redrawTextBoundingBox = (
} }
boundTextUpdates.height = metrics.height; boundTextUpdates.height = metrics.height;
// Maintain coordX for non left-aligned text in case the width has changed
if (!container) {
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
boundTextUpdates.x += textElement.width - metrics.width;
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
}
}
if (container) { if (container) {
const maxContainerHeight = getBoundTextMaxHeight( const maxContainerHeight = getBoundTextMaxHeight(
container, container,
@@ -219,9 +191,17 @@ export const handleBindTextResize = (
(transformHandleType !== "n" && transformHandleType !== "s") (transformHandleType !== "n" && transformHandleType !== "s")
) { ) {
if (text) { if (text) {
text = wrapTextElement(textElement, maxWidth); text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
} }
const metrics = measureTextElement(textElement, { text }); const metrics = measureText(
text,
getFontString(textElement),
textElement.lineHeight,
);
nextHeight = metrics.height; nextHeight = metrics.height;
nextWidth = metrics.width; nextWidth = metrics.width;
} }
@@ -19,7 +19,7 @@ import type {
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getOriginalContainerHeightFromCache } from "./containerCache";
import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
import { pointFrom } from "../../math"; import { point } from "../../math";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -42,7 +42,7 @@ describe("textWysiwyg", () => {
type: "line", type: "line",
width: 100, width: 100,
height: 0, height: 0,
points: [pointFrom(0, 0), pointFrom(100, 0)], points: [point(0, 0), point(100, 0)],
}); });
const textSize = 20; const textSize = 20;
const text = API.createElement({ const text = API.createElement({
+6 -58
View File
@@ -25,7 +25,6 @@ import {
getContainerElement, getContainerElement,
getTextElementAngle, getTextElementAngle,
getTextWidth, getTextWidth,
measureText,
normalizeText, normalizeText,
redrawTextBoundingBox, redrawTextBoundingBox,
wrapText, wrapText,
@@ -47,15 +46,12 @@ import {
import type App from "../components/App"; import type App from "../components/App";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard"; import { parseClipboard } from "../clipboard";
import type { SubtypeMethods } from "./subtypes";
import { getSubtypeMethods } from "./subtypes";
import { import {
originalContainerCache, originalContainerCache,
updateOriginalContainerCache, updateOriginalContainerCache,
} from "./containerCache"; } from "./containerCache";
const getTransform = ( const getTransform = (
offsetX: number,
width: number, width: number,
height: number, height: number,
angle: number, angle: number,
@@ -73,18 +69,9 @@ const getTransform = (
if (height > maxHeight && zoom.value !== 1) { if (height > maxHeight && zoom.value !== 1) {
translateY = (maxHeight * (zoom.value - 1)) / 2; translateY = (maxHeight * (zoom.value - 1)) / 2;
} }
const offset = offsetX !== 0 ? ` translate(${offsetX}px, 0px)` : ""; return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)${offset}`;
}; };
const getEditorStyle = function (element) {
const map = getSubtypeMethods(element.subtype);
if (map?.getEditorStyle) {
return map.getEditorStyle(element);
}
return {};
} as SubtypeMethods["getEditorStyle"];
export const textWysiwyg = ({ export const textWysiwyg = ({
id, id,
onChange, onChange,
@@ -150,27 +137,14 @@ export const textWysiwyg = ({
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
); );
// Editing metrics let width = updatedTextElement.width;
const eMetrics = measureText(
container && updatedTextElement.containerId
? wrapText(
updatedTextElement.originalText,
getFontString(updatedTextElement),
getBoundTextMaxWidth(container, updatedTextElement),
)
: updatedTextElement.originalText,
getFontString(updatedTextElement),
updatedTextElement.lineHeight,
);
let width = Math.max(updatedTextElement.width, eMetrics.width); // set to element height by default since that's
// Set to element height by default since that's
// what is going to be used for unbounded text // what is going to be used for unbounded text
let height = Math.max(updatedTextElement.height, eMetrics.height); let height = updatedTextElement.height;
let maxWidth = width; let maxWidth = updatedTextElement.width;
let maxHeight = height; let maxHeight = updatedTextElement.height;
if (container && updatedTextElement.containerId) { if (container && updatedTextElement.containerId) {
if (isArrowElement(container)) { if (isArrowElement(container)) {
@@ -266,31 +240,8 @@ export const textWysiwyg = ({
width += 0.5; width += 0.5;
} }
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
const offWidth = container
? Math.min(
0,
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
)
: Math.min(maxWidth, updatedTextElement.width) -
Math.min(maxWidth, eMetrics.width);
const offsetX =
textAlign === "right"
? offWidth
: textAlign === "center"
? offWidth / 2
: 0;
let { width: w, height: h } = updatedTextElement;
// add 5% buffer otherwise it causes wysiwyg to jump // add 5% buffer otherwise it causes wysiwyg to jump
height *= 1.05; height *= 1.05;
h *= 1.05;
const transformOrigin =
updatedTextElement.width !== eMetrics.width ||
updatedTextElement.height !== eMetrics.height
? { transformOrigin: `${w / 2}px ${h / 2}px` }
: {};
const font = getFontString(updatedTextElement); const font = getFontString(updatedTextElement);
@@ -310,9 +261,7 @@ export const textWysiwyg = ({
height: `${height}px`, height: `${height}px`,
left: `${viewportX - padding}px`, left: `${viewportX - padding}px`,
top: `${viewportY}px`, top: `${viewportY}px`,
...transformOrigin,
transform: getTransform( transform: getTransform(
offsetX,
width, width,
height, height,
getTextElementAngle(updatedTextElement, container), getTextElementAngle(updatedTextElement, container),
@@ -373,7 +322,6 @@ export const textWysiwyg = ({
whiteSpace, whiteSpace,
overflowWrap: "break-word", overflowWrap: "break-word",
boxSizing: "content-box", boxSizing: "content-box",
...getEditorStyle(element),
}); });
editable.value = element.originalText; editable.value = element.originalText;
updateWysiwygStyle(); updateWysiwygStyle();
@@ -19,7 +19,7 @@ import {
isIOS, isIOS,
} from "../constants"; } from "../constants";
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { pointFrom, pointRotateRads } from "../../math"; import { point, pointRotateRads } from "../../math";
export type TransformHandleDirection = export type TransformHandleDirection =
| "n" | "n"
@@ -95,8 +95,8 @@ const generateTransformHandle = (
angle: Radians, angle: Radians,
): TransformHandle => { ): TransformHandle => {
const [xx, yy] = pointRotateRads( const [xx, yy] = pointRotateRads(
pointFrom(x + width / 2, y + height / 2), point(x + width / 2, y + height / 2),
pointFrom(cx, cy), point(cx, cy),
angle, angle,
); );
return [xx - width / 2, yy - height / 2, width, height]; return [xx - width / 2, yy - height / 2, width, height];
-1
View File
@@ -76,7 +76,6 @@ type _ExcalidrawElementBase = Readonly<{
updated: number; updated: number;
link: string | null; link: string | null;
locked: boolean; locked: boolean;
subtype?: string;
customData?: Record<string, any>; customData?: Record<string, any>;
}>; }>;
+7 -7
View File
@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
import ComicShanns from "./assets/ComicShanns-Regular.woff2"; import ComicShanns from "./assets/ComicShanns-Regular.woff2";
import LiberationSans from "./assets/LiberationSans-Regular.woff2"; import LiberationSans from "./assets/LiberationSans-Regular.woff2";
import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
export class Fonts { export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use // it's ok to track fonts across multiple instances only once, so let's use
+4 -4
View File
@@ -29,7 +29,7 @@ import { getElementLineSegments } from "./element/bounds";
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/"; import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import type { ReadonlySetLike } from "./utility-types"; import type { ReadonlySetLike } from "./utility-types";
import { isPointWithinBounds, pointFrom } from "../math"; import { isPointWithinBounds, point } from "../math";
// --------------------------- Frame State ------------------------------------ // --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = ( export const bindElementsToFramesAfterDuplication = (
@@ -159,9 +159,9 @@ export const isCursorInFrame = (
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
return isPointWithinBounds( return isPointWithinBounds(
pointFrom(fx1, fy1), point(fx1, fy1),
pointFrom(cursorCoords.x, cursorCoords.y), point(cursorCoords.x, cursorCoords.y),
pointFrom(fx2, fy2), point(fx2, fy2),
); );
}; };
-2
View File
@@ -104,5 +104,3 @@ declare namespace jest {
toBeNonNaNNumber(): void; toBeNonNaNNumber(): void;
} }
} }
declare module "mathjax-full/js/input/asciimath/mathjax2/legacy/MathJax";
+1 -23
View File
@@ -87,17 +87,6 @@ if (import.meta.env.DEV) {
let currentLang: Language = defaultLang; let currentLang: Language = defaultLang;
let currentLangData = {}; let currentLangData = {};
let fallbackCustomLangData = {};
const langLoaders: LangLdr[] = [];
export type LangLdr = (langCode: string) => Promise<{}>;
export const registerCustomLangData = (fallbackLangData: {}, ldr: LangLdr) => {
if (!langLoaders.includes(ldr)) {
fallbackCustomLangData = { ...fallbackLangData, ...fallbackCustomLangData };
langLoaders.push(ldr);
}
};
export const setLanguage = async (lang: Language) => { export const setLanguage = async (lang: Language) => {
currentLang = lang; currentLang = lang;
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr"; document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
@@ -112,14 +101,6 @@ export const setLanguage = async (lang: Language) => {
console.error(`Failed to load language ${lang.code}:`, error.message); console.error(`Failed to load language ${lang.code}:`, error.message);
currentLangData = fallbackLangData; currentLangData = fallbackLangData;
} }
const auxData = langLoaders.map((fn) => fn(currentLang.code));
while (auxData.length > 0) {
try {
currentLangData = { ...(await auxData.pop()), ...currentLangData };
} catch (error: any) {
console.error(`Error loading ${lang.code} extra data:`, error.message);
}
}
} }
jotaiStore.set(editorLangCodeAtom, lang.code); jotaiStore.set(editorLangCodeAtom, lang.code);
@@ -142,9 +123,7 @@ const findPartsForData = (data: any, parts: string[]) => {
}; };
export const t = ( export const t = (
path: path: NestedKeyOf<typeof fallbackLangData>,
| NestedKeyOf<typeof fallbackLangData>
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
replacement?: { [key: string]: string | number } | null, replacement?: { [key: string]: string | number } | null,
fallback?: string, fallback?: string,
) => { ) => {
@@ -159,7 +138,6 @@ export const t = (
let translation = let translation =
findPartsForData(currentLangData, parts) || findPartsForData(currentLangData, parts) ||
findPartsForData(fallbackLangData, parts) || findPartsForData(fallbackLangData, parts) ||
findPartsForData(fallbackCustomLangData, parts) ||
fallback; fallback;
if (translation === undefined) { if (translation === undefined) {
const errorMessage = `Can't find translation for ${path}`; const errorMessage = `Can't find translation for ${path}`;
+2 -1
View File
@@ -230,7 +230,8 @@
"resetLibrary": "This will clear your library. Are you sure?", "resetLibrary": "This will clear your library. Are you sure?",
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?", "removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.", "invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!" "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!",
"saveYourContent": "Don't forget to save your content ({{shortcut}})!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Unsupported file type.", "unsupportedFileType": "Unsupported file type.",
-1
View File
@@ -72,7 +72,6 @@
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",
"jotai": "1.13.1", "jotai": "1.13.1",
"lodash.throttle": "4.1.1", "lodash.throttle": "4.1.1",
"mathjax-full": "3.2.2",
"nanoid": "3.3.3", "nanoid": "3.3.3",
"open-color": "1.9.1", "open-color": "1.9.1",
"pako": "1.0.11", "pako": "1.0.11",
+5 -38
View File
@@ -23,6 +23,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
import type { import type {
StaticCanvasRenderConfig, StaticCanvasRenderConfig,
RenderableElementsMap,
InteractiveCanvasRenderConfig, InteractiveCanvasRenderConfig,
} from "../scene/types"; } from "../scene/types";
import { distance, getFontString, isRTL } from "../utils"; import { distance, getFontString, isRTL } from "../utils";
@@ -36,7 +37,6 @@ import type {
PendingExcalidrawElements, PendingExcalidrawElements,
} from "../types"; } from "../types";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { getSubtypeMethods } from "../element/subtypes";
import { import {
BOUND_TEXT_PADDING, BOUND_TEXT_PADDING,
ELEMENT_READY_TO_ERASE_OPACITY, ELEMENT_READY_TO_ERASE_OPACITY,
@@ -251,14 +251,7 @@ const generateElementCanvas = (
context.filter = IMAGE_INVERT_FILTER; context.filter = IMAGE_INVERT_FILTER;
} }
drawElementOnCanvas( drawElementOnCanvas(element, rc, context, renderConfig, appState);
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
context.restore(); context.restore();
@@ -381,22 +374,11 @@ const drawImagePlaceholder = (
const drawElementOnCanvas = ( const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
context.globalAlpha =
((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
element.opacity) /
10000;
const map = getSubtypeMethods(element.subtype);
if (map?.render) {
map.render(element, elementsMap, context);
context.globalAlpha = 1;
return;
}
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
case "iframe": case "iframe":
@@ -691,7 +673,7 @@ export const renderSelectionElement = (
export const renderElement = ( export const renderElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap, elementsMap: RenderableElementsMap,
allElementsMap: NonDeletedSceneElementsMap, allElementsMap: NonDeletedSceneElementsMap,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
@@ -760,14 +742,7 @@ export const renderElement = (
context.translate(cx, cy); context.translate(cx, cy);
context.rotate(element.angle); context.rotate(element.angle);
context.translate(-shiftX, -shiftY); context.translate(-shiftX, -shiftY);
drawElementOnCanvas( drawElementOnCanvas(element, rc, context, renderConfig, appState);
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
context.restore(); context.restore();
} else { } else {
const elementWithCanvas = generateElementWithCanvas( const elementWithCanvas = generateElementWithCanvas(
@@ -862,7 +837,6 @@ export const renderElement = (
drawElementOnCanvas( drawElementOnCanvas(
element, element,
elementsMap,
tempRc, tempRc,
tempCanvasContext, tempCanvasContext,
renderConfig, renderConfig,
@@ -906,14 +880,7 @@ export const renderElement = (
} }
context.translate(-shiftX, -shiftY); context.translate(-shiftX, -shiftY);
drawElementOnCanvas( drawElementOnCanvas(element, rc, context, renderConfig, appState);
element,
elementsMap,
rc,
context,
renderConfig,
appState,
);
} }
context.restore(); context.restore();
+15 -23
View File
@@ -1,4 +1,4 @@
import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math"; import { point, type GlobalPoint, type LocalPoint } from "../../math";
import { THEME } from "../constants"; import { THEME } from "../constants";
import type { PointSnapLine, PointerSnapLine } from "../snapping"; import type { PointSnapLine, PointerSnapLine } from "../snapping";
import type { InteractiveCanvasAppState } from "../types"; import type { InteractiveCanvasAppState } from "../types";
@@ -140,31 +140,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
// (1) // (1)
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
drawLine( drawLine(
pointFrom(from[0], from[1] - FULL), point(from[0], from[1] - FULL),
pointFrom(from[0], from[1] + FULL), point(from[0], from[1] + FULL),
context, context,
); );
} }
// (3) // (3)
drawLine( drawLine(
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF), point(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF), point(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
context, context,
); );
drawLine( drawLine(
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF), point(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF), point(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
context, context,
); );
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
// (4) // (4)
drawLine( drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context);
pointFrom(to[0], to[1] - FULL),
pointFrom(to[0], to[1] + FULL),
context,
);
// (2) // (2)
drawLine(from, to, context); drawLine(from, to, context);
@@ -174,31 +170,27 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
// (1) // (1)
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
drawLine( drawLine(
pointFrom(from[0] - FULL, from[1]), point(from[0] - FULL, from[1]),
pointFrom(from[0] + FULL, from[1]), point(from[0] + FULL, from[1]),
context, context,
); );
} }
// (3) // (3)
drawLine( drawLine(
pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER), point(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER), point(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
context, context,
); );
drawLine( drawLine(
pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER), point(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER), point(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
context, context,
); );
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
// (4) // (4)
drawLine( drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context);
pointFrom(to[0] - FULL, to[1]),
pointFrom(to[0] + FULL, to[1]),
context,
);
// (2) // (2)
drawLine(from, to, context); drawLine(from, to, context);
@@ -35,7 +35,6 @@ import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
import type { AppState, BinaryFiles } from "../types"; import type { AppState, BinaryFiles } from "../types";
import { getFontFamilyString, isRTL, isTestEnv } from "../utils"; import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getSubtypeMethods } from "../element/subtypes";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes"; import { getCornerRadius, isPathALoop } from "../shapes";
@@ -126,15 +125,6 @@ const renderElementToSvg = (
root.appendChild(node); root.appendChild(node);
}; };
const map = getSubtypeMethods(element.subtype);
if (map?.renderSvg) {
map.renderSvg(svgRoot, addToRoot, element, elementsMap, {
offsetX,
offsetY,
});
return;
}
const opacity = const opacity =
((getContainingFrame(element, elementsMap)?.opacity ?? 100) * ((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
element.opacity) / element.opacity) /
+2 -2
View File
@@ -24,7 +24,7 @@ import {
import { canChangeRoundness } from "./comparisons"; import { canChangeRoundness } from "./comparisons";
import type { EmbedsValidationStatus } from "../types"; import type { EmbedsValidationStatus } from "../types";
import { import {
pointFrom, point,
pointDistance, pointDistance,
type GlobalPoint, type GlobalPoint,
type LocalPoint, type LocalPoint,
@@ -408,7 +408,7 @@ export const _generateElementShape = (
// initial position to it // initial position to it
const points = element.points.length const points = element.points.length
? element.points ? element.points
: [pointFrom<LocalPoint>(0, 0)]; : [point<LocalPoint>(0, 0)];
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
shape = [ shape = [
+26 -26
View File
@@ -1,6 +1,6 @@
import { import {
isPoint, isPoint,
pointFrom, point,
pointDistance, pointDistance,
pointFromPair, pointFromPair,
pointRotateRads, pointRotateRads,
@@ -167,15 +167,15 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
? getClosedCurveShape<Point>( ? getClosedCurveShape<Point>(
element, element,
roughShape, roughShape,
pointFrom<Point>(element.x, element.y), point<Point>(element.x, element.y),
element.angle, element.angle,
pointFrom(cx, cy), point(cx, cy),
) )
: getCurveShape<Point>( : getCurveShape<Point>(
roughShape, roughShape,
pointFrom<Point>(element.x, element.y), point<Point>(element.x, element.y),
element.angle, element.angle,
pointFrom(cx, cy), point(cx, cy),
); );
} }
@@ -186,7 +186,7 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
return getFreedrawShape( return getFreedrawShape(
element, element,
pointFrom(cx, cy), point(cx, cy),
shouldTestInside(element), shouldTestInside(element),
); );
} }
@@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = <
} }
const ops = getCurvePathOps(shape[0]); const ops = getCurvePathOps(shape[0]);
let currentP = pointFrom<P>(0, 0); let currentP = point<P>(0, 0);
let index = 0; let index = 0;
let minDistance = Infinity; let minDistance = Infinity;
let controlPoints: P[] | null = null; let controlPoints: P[] | null = null;
@@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = <
} }
if (op === "bcurveTo") { if (op === "bcurveTo") {
const p0 = currentP; const p0 = currentP;
const p1 = pointFrom<P>(data[0], data[1]); const p1 = point<P>(data[0], data[1]);
const p2 = pointFrom<P>(data[2], data[3]); const p2 = point<P>(data[2], data[3]);
const p3 = pointFrom<P>(data[4], data[5]); const p3 = point<P>(data[4], data[5]);
const distance = pointDistance(p3, endPoint); const distance = pointDistance(p3, endPoint);
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
@@ -279,7 +279,7 @@ export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
p0[idx] * Math.pow(t, 3); p0[idx] * Math.pow(t, 3);
const tx = equation(t, 0); const tx = equation(t, 0);
const ty = equation(t, 1); const ty = equation(t, 1);
return pointFrom(tx, ty); return point(tx, ty);
}; };
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>( const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
@@ -301,12 +301,12 @@ const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
controlPoints[3], controlPoints[3],
t, t,
); );
pointsOnCurve.push(pointFrom(p[0], p[1])); pointsOnCurve.push(point(p[0], p[1]));
t -= 0.05; t -= 0.05;
} }
if (pointsOnCurve.length) { if (pointsOnCurve.length) {
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) { if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1])); pointsOnCurve.push(point(endPoint[0], endPoint[1]));
} }
} }
return pointsOnCurve; return pointsOnCurve;
@@ -393,24 +393,24 @@ export const aabbForElement = (
midY: element.y + element.height / 2, midY: element.y + element.height / 2,
}; };
const center = pointFrom(bbox.midX, bbox.midY); const center = point(bbox.midX, bbox.midY);
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(bbox.minX, bbox.minY), point(bbox.minX, bbox.minY),
center, center,
element.angle, element.angle,
); );
const [topRightX, topRightY] = pointRotateRads( const [topRightX, topRightY] = pointRotateRads(
pointFrom(bbox.maxX, bbox.minY), point(bbox.maxX, bbox.minY),
center, center,
element.angle, element.angle,
); );
const [bottomRightX, bottomRightY] = pointRotateRads( const [bottomRightX, bottomRightY] = pointRotateRads(
pointFrom(bbox.maxX, bbox.maxY), point(bbox.maxX, bbox.maxY),
center, center,
element.angle, element.angle,
); );
const [bottomLeftX, bottomLeftY] = pointRotateRads( const [bottomLeftX, bottomLeftY] = pointRotateRads(
pointFrom(bbox.minX, bbox.maxY), point(bbox.minX, bbox.maxY),
center, center,
element.angle, element.angle,
); );
@@ -442,14 +442,14 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
export const aabbsOverlapping = (a: Bounds, b: Bounds) => export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
pointInsideBounds(pointFrom(a[0], a[1]), b) || pointInsideBounds(point(a[0], a[1]), b) ||
pointInsideBounds(pointFrom(a[2], a[1]), b) || pointInsideBounds(point(a[2], a[1]), b) ||
pointInsideBounds(pointFrom(a[2], a[3]), b) || pointInsideBounds(point(a[2], a[3]), b) ||
pointInsideBounds(pointFrom(a[0], a[3]), b) || pointInsideBounds(point(a[0], a[3]), b) ||
pointInsideBounds(pointFrom(b[0], b[1]), a) || pointInsideBounds(point(b[0], b[1]), a) ||
pointInsideBounds(pointFrom(b[2], b[1]), a) || pointInsideBounds(point(b[2], b[1]), a) ||
pointInsideBounds(pointFrom(b[2], b[3]), a) || pointInsideBounds(point(b[2], b[3]), a) ||
pointInsideBounds(pointFrom(b[0], b[3]), a); pointInsideBounds(point(b[0], b[3]), a);
export const getCornerRadius = (x: number, element: ExcalidrawElement) => { export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
if ( if (
+65 -89
View File
@@ -1,6 +1,6 @@
import type { InclusiveRange } from "../math"; import type { InclusiveRange } from "../math";
import { import {
pointFrom, point,
pointRotateRads, pointRotateRads,
rangeInclusive, rangeInclusive,
rangeIntersection, rangeIntersection,
@@ -228,52 +228,52 @@ export const getElementsCorners = (
!boundingBoxCorners !boundingBoxCorners
) { ) {
const leftMid = pointRotateRads<GlobalPoint>( const leftMid = pointRotateRads<GlobalPoint>(
pointFrom(x1, y1 + halfHeight), point(x1, y1 + halfHeight),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const topMid = pointRotateRads<GlobalPoint>( const topMid = pointRotateRads<GlobalPoint>(
pointFrom(x1 + halfWidth, y1), point(x1 + halfWidth, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const rightMid = pointRotateRads<GlobalPoint>( const rightMid = pointRotateRads<GlobalPoint>(
pointFrom(x2, y1 + halfHeight), point(x2, y1 + halfHeight),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const bottomMid = pointRotateRads<GlobalPoint>( const bottomMid = pointRotateRads<GlobalPoint>(
pointFrom(x1 + halfWidth, y2), point(x1 + halfWidth, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const center = pointFrom<GlobalPoint>(cx, cy); const center = point<GlobalPoint>(cx, cy);
result = omitCenter result = omitCenter
? [leftMid, topMid, rightMid, bottomMid] ? [leftMid, topMid, rightMid, bottomMid]
: [leftMid, topMid, rightMid, bottomMid, center]; : [leftMid, topMid, rightMid, bottomMid, center];
} else { } else {
const topLeft = pointRotateRads<GlobalPoint>( const topLeft = pointRotateRads<GlobalPoint>(
pointFrom(x1, y1), point(x1, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const topRight = pointRotateRads<GlobalPoint>( const topRight = pointRotateRads<GlobalPoint>(
pointFrom(x2, y1), point(x2, y1),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const bottomLeft = pointRotateRads<GlobalPoint>( const bottomLeft = pointRotateRads<GlobalPoint>(
pointFrom(x1, y2), point(x1, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const bottomRight = pointRotateRads<GlobalPoint>( const bottomRight = pointRotateRads<GlobalPoint>(
pointFrom(x2, y2), point(x2, y2),
pointFrom(cx, cy), point(cx, cy),
element.angle, element.angle,
); );
const center = pointFrom<GlobalPoint>(cx, cy); const center = point<GlobalPoint>(cx, cy);
result = omitCenter result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight] ? [topLeft, topRight, bottomLeft, bottomRight]
@@ -287,18 +287,18 @@ export const getElementsCorners = (
const width = maxX - minX; const width = maxX - minX;
const height = maxY - minY; const height = maxY - minY;
const topLeft = pointFrom<GlobalPoint>(minX, minY); const topLeft = point<GlobalPoint>(minX, minY);
const topRight = pointFrom<GlobalPoint>(maxX, minY); const topRight = point<GlobalPoint>(maxX, minY);
const bottomLeft = pointFrom<GlobalPoint>(minX, maxY); const bottomLeft = point<GlobalPoint>(minX, maxY);
const bottomRight = pointFrom<GlobalPoint>(maxX, maxY); const bottomRight = point<GlobalPoint>(maxX, maxY);
const center = pointFrom<GlobalPoint>(minX + width / 2, minY + height / 2); const center = point<GlobalPoint>(minX + width / 2, minY + height / 2);
result = omitCenter result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight] ? [topLeft, topRight, bottomLeft, bottomRight]
: [topLeft, topRight, bottomLeft, bottomRight, center]; : [topLeft, topRight, bottomLeft, bottomRight, center];
} }
return result.map((p) => pointFrom(round(p[0]), round(p[1]))); return result.map((p) => point(round(p[0]), round(p[1])));
}; };
const getReferenceElements = ( const getReferenceElements = (
@@ -375,11 +375,8 @@ export const getVisibleGaps = (
horizontalGaps.push({ horizontalGaps.push({
startBounds, startBounds,
endBounds, endBounds,
startSide: [ startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
pointFrom(startMaxX, startMinY), endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
pointFrom(startMaxX, startMaxY),
],
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)],
length: endMinX - startMaxX, length: endMinX - startMaxX,
overlap: rangeIntersection( overlap: rangeIntersection(
rangeInclusive(startMinY, startMaxY), rangeInclusive(startMinY, startMaxY),
@@ -418,11 +415,8 @@ export const getVisibleGaps = (
verticalGaps.push({ verticalGaps.push({
startBounds, startBounds,
endBounds, endBounds,
startSide: [ startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
pointFrom(startMinX, startMaxY), endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
pointFrom(startMaxX, startMaxY),
],
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)],
length: endMinY - startMaxY, length: endMinY - startMaxY,
overlap: rangeIntersection( overlap: rangeIntersection(
rangeInclusive(startMinX, startMaxX), rangeInclusive(startMinX, startMaxX),
@@ -838,7 +832,7 @@ const createPointSnapLines = (
} }
snapsX[key].push( snapsX[key].push(
...snap.points.map((p) => ...snap.points.map((p) =>
pointFrom<GlobalPoint>(round(p[0]), round(p[1])), point<GlobalPoint>(round(p[0]), round(p[1])),
), ),
); );
} }
@@ -855,7 +849,7 @@ const createPointSnapLines = (
} }
snapsY[key].push( snapsY[key].push(
...snap.points.map((p) => ...snap.points.map((p) =>
pointFrom<GlobalPoint>(round(p[0]), round(p[1])), point<GlobalPoint>(round(p[0]), round(p[1])),
), ),
); );
} }
@@ -869,7 +863,7 @@ const createPointSnapLines = (
points: dedupePoints( points: dedupePoints(
points points
.map((p) => { .map((p) => {
return pointFrom<GlobalPoint>(Number(key), p[1]); return point<GlobalPoint>(Number(key), p[1]);
}) })
.sort((a, b) => a[1] - b[1]), .sort((a, b) => a[1] - b[1]),
), ),
@@ -882,7 +876,7 @@ const createPointSnapLines = (
points: dedupePoints( points: dedupePoints(
points points
.map((p) => { .map((p) => {
return pointFrom<GlobalPoint>(p[0], Number(key)); return point<GlobalPoint>(p[0], Number(key));
}) })
.sort((a, b) => a[0] - b[0]), .sort((a, b) => a[0] - b[0]),
), ),
@@ -946,16 +940,16 @@ const createGapSnapLines = (
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [
pointFrom(gapSnap.gap.startSide[0][0], gapLineY), point(gapSnap.gap.startSide[0][0], gapLineY),
pointFrom(minX, gapLineY), point(minX, gapLineY),
], ],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [
pointFrom(maxX, gapLineY), point(maxX, gapLineY),
pointFrom(gapSnap.gap.endSide[0][0], gapLineY), point(gapSnap.gap.endSide[0][0], gapLineY),
], ],
}, },
); );
@@ -972,16 +966,16 @@ const createGapSnapLines = (
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [
pointFrom(gapLineX, gapSnap.gap.startSide[0][1]), point(gapLineX, gapSnap.gap.startSide[0][1]),
pointFrom(gapLineX, minY), point(gapLineX, minY),
], ],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [
pointFrom(gapLineX, maxY), point(gapLineX, maxY),
pointFrom(gapLineX, gapSnap.gap.endSide[0][1]), point(gapLineX, gapSnap.gap.endSide[0][1]),
], ],
}, },
); );
@@ -997,15 +991,12 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
pointFrom(startMaxX, gapLineY),
pointFrom(endMinX, gapLineY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)], points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
}, },
); );
} }
@@ -1020,18 +1011,12 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
pointFrom(maxX, gapLineY),
pointFrom(startMinX, gapLineY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
pointFrom(startMaxX, gapLineY),
pointFrom(endMinX, gapLineY),
],
}, },
); );
} }
@@ -1046,18 +1031,12 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
pointFrom(gapLineX, maxY),
pointFrom(gapLineX, startMinY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
pointFrom(gapLineX, startMaxY),
pointFrom(gapLineX, endMinY),
],
}, },
); );
} }
@@ -1072,15 +1051,12 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
pointFrom(gapLineX, startMaxY),
pointFrom(gapLineX, endMinY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)], points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
}, },
); );
} }
@@ -1094,7 +1070,7 @@ const createGapSnapLines = (
return { return {
...gapSnapLine, ...gapSnapLine,
points: gapSnapLine.points.map((p) => points: gapSnapLine.points.map((p) =>
pointFrom(round(p[0]), round(p[1])), point(round(p[0]), round(p[1])),
) as PointPair, ) as PointPair,
}; };
}), }),
@@ -1144,35 +1120,35 @@ export const snapResizingElements = (
if (transformHandle) { if (transformHandle) {
switch (transformHandle) { switch (transformHandle) {
case "e": { case "e": {
selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY)); selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
break; break;
} }
case "w": { case "w": {
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY)); selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
break; break;
} }
case "n": { case "n": {
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY)); selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
break; break;
} }
case "s": { case "s": {
selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY)); selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
break; break;
} }
case "ne": { case "ne": {
selectionSnapPoints.push(pointFrom(maxX, minY)); selectionSnapPoints.push(point(maxX, minY));
break; break;
} }
case "nw": { case "nw": {
selectionSnapPoints.push(pointFrom(minX, minY)); selectionSnapPoints.push(point(minX, minY));
break; break;
} }
case "se": { case "se": {
selectionSnapPoints.push(pointFrom(maxX, maxY)); selectionSnapPoints.push(point(maxX, maxY));
break; break;
} }
case "sw": { case "sw": {
selectionSnapPoints.push(pointFrom(minX, maxY)); selectionSnapPoints.push(point(minX, maxY));
break; break;
} }
} }
@@ -1215,10 +1191,10 @@ export const snapResizingElements = (
); );
const corners: GlobalPoint[] = [ const corners: GlobalPoint[] = [
pointFrom(x1, y1), point(x1, y1),
pointFrom(x1, y2), point(x1, y2),
pointFrom(x2, y1), point(x2, y1),
pointFrom(x2, y2), point(x2, y2),
]; ];
getPointSnaps( getPointSnaps(
@@ -1255,7 +1231,7 @@ export const snapNewElement = (
} }
const selectionSnapPoints: GlobalPoint[] = [ const selectionSnapPoints: GlobalPoint[] = [
pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y), point(origin.x + dragOffset.x, origin.y + dragOffset.y),
]; ];
const snapDistance = getSnapDistance(app.state.zoom.value); const snapDistance = getSnapDistance(app.state.zoom.value);
@@ -1355,7 +1331,7 @@ export const getSnapLinesAtPointer = (
verticalSnapLines.push({ verticalSnapLines.push({
type: "pointer", type: "pointer",
points: [corner, pointFrom(corner[0], pointer.y)], points: [corner, point(corner[0], pointer.y)],
direction: "vertical", direction: "vertical",
}); });
@@ -1371,7 +1347,7 @@ export const getSnapLinesAtPointer = (
horizontalSnapLines.push({ horizontalSnapLines.push({
type: "pointer", type: "pointer",
points: [corner, pointFrom(pointer.x, corner[1])], points: [corner, point(pointer.x, corner[1])],
direction: "horizontal", direction: "horizontal",
}); });
+4 -9
View File
@@ -7,7 +7,7 @@ import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { pointFrom } from "../../math"; import { point } from "../../math";
const { h } = window; const { h } = window;
@@ -32,12 +32,7 @@ describe("element binding", () => {
y: 0, y: 0,
width: 100, width: 100,
height: 1, height: 1,
points: [ points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)],
pointFrom(0, 0),
pointFrom(0, 0),
pointFrom(100, 0),
pointFrom(100, 0),
],
}); });
API.setElements([rect, arrow]); API.setElements([rect, arrow]);
expect(arrow.startBinding).toBe(null); expect(arrow.startBinding).toBe(null);
@@ -315,7 +310,7 @@ describe("element binding", () => {
const arrow1 = API.createElement({ const arrow1 = API.createElement({
type: "arrow", type: "arrow",
id: "arrow1", id: "arrow1",
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], points: [point(0, 0), point(0, -87.45777932247563)],
startBinding: { startBinding: {
elementId: "rectangle1", elementId: "rectangle1",
focus: 0.2, focus: 0.2,
@@ -333,7 +328,7 @@ describe("element binding", () => {
const arrow2 = API.createElement({ const arrow2 = API.createElement({
type: "arrow", type: "arrow",
id: "arrow2", id: "arrow2",
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], points: [point(0, 0), point(0, -87.45777932247563)],
startBinding: { startBinding: {
elementId: "text1", elementId: "text1",
focus: 0.2, focus: 0.2,
@@ -1,94 +0,0 @@
import type { ExcalidrawElement } from "../element/types";
import { getShortcutKey } from "../utils";
import { API } from "./helpers/api";
import { render } from "./test-utils";
import { Excalidraw } from "../index";
import {
getShortcutFromShortcutName,
registerCustomShortcuts,
} from "../actions/shortcuts";
import type {
Action,
ActionPredicateFn,
ActionResult,
CustomActionName,
} from "../actions/types";
import { makeCustomActionName } from "../actions/types";
import {
actionChangeFontFamily,
actionChangeFontSize,
} from "../actions/actionProperties";
import { isTextElement } from "../element";
const { h } = window;
describe("regression tests", () => {
it("should retrieve custom shortcuts", () => {
const shortcutName = makeCustomActionName("test");
const shortcuts: Record<CustomActionName, string[]> = {};
shortcuts[shortcutName] = [
getShortcutKey("CtrlOrCmd+1"),
getShortcutKey("CtrlOrCmd+2"),
];
registerCustomShortcuts(shortcuts);
expect(getShortcutFromShortcutName(shortcutName)).toBe("Ctrl+1");
});
it("should apply universal action predicates", async () => {
await render(<Excalidraw />);
// Create the test elements
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
const el3 = API.createElement({ type: "text", id: "C", y: 60 });
const el12: ExcalidrawElement[] = [el1, el2];
const el13: ExcalidrawElement[] = [el1, el3];
const el23: ExcalidrawElement[] = [el2, el3];
const el123: ExcalidrawElement[] = [el1, el2, el3];
// Set up the custom Action enablers
const enableName = "custom.enable";
const enableAction: Action = {
name: enableName,
label: "",
perform: (): ActionResult => {
return {} as ActionResult;
},
trackEvent: false,
};
const enabler: ActionPredicateFn = function (action, elements) {
if (action.name !== enableName || elements.some((el) => el.y === 30)) {
return true;
}
return false;
};
// Set up the standard Action disablers
const disabled1 = actionChangeFontFamily;
const disabled2 = actionChangeFontSize;
const disabler: ActionPredicateFn = function (action, elements) {
if (
action.name === disabled2.name &&
elements.some((el) => el.y === 0 || isTextElement(el))
) {
return false;
}
return true;
};
// Test the custom Action enablers
const am = h.app.actionManager;
am.registerActionPredicate(enabler);
expect(am.isActionEnabled(enableAction, { elements: el12 })).toBe(true);
expect(am.isActionEnabled(enableAction, { elements: el13 })).toBe(false);
expect(am.isActionEnabled(enableAction, { elements: el23 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el12 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el13 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el23 })).toBe(true);
// Test the standard Action disablers
am.registerActionPredicate(disabler);
expect(am.isActionEnabled(disabled1, { elements: el123 })).toBe(true);
expect(am.isActionEnabled(disabled2, { elements: [el1] })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: [el2] })).toBe(true);
expect(am.isActionEnabled(disabled2, { elements: [el3] })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el12 })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el23 })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el13 })).toBe(false);
});
});
+4 -4
View File
@@ -28,7 +28,7 @@ import { getBoundTextElementPosition } from "../element/textElement";
import { createPasteEvent } from "../clipboard"; import { createPasteEvent } from "../clipboard";
import { arrayToMap, cloneJSON } from "../utils"; import { arrayToMap, cloneJSON } from "../utils";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom, type Radians } from "../../math"; import { point, type Radians } from "../../math";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -146,9 +146,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
link: null, link: null,
locked: false, locked: false,
points: [ points: [
pointFrom<LocalPoint>(0, 0), point<LocalPoint>(0, 0),
pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625), point<LocalPoint>(-922.4761962890625, 300.3277587890625),
pointFrom<LocalPoint>(828.0126953125, 410.51605224609375), point<LocalPoint>(828.0126953125, 410.51605224609375),
], ],
}); });
}; };
+5 -40
View File
@@ -20,18 +20,7 @@ import fs from "fs";
import util from "util"; import util from "util";
import path from "path"; import path from "path";
import { getMimeType } from "../../data/blob"; import { getMimeType } from "../../data/blob";
import type {
SubtypeLoadedCb,
SubtypePrepFn,
SubtypeRecord,
} from "../../element/subtypes";
import { import {
checkRefreshOnSubtypeLoad,
prepareSubtype,
selectSubtype,
} from "../../element/subtypes";
import {
maybeGetSubtypeProps,
newArrowElement, newArrowElement,
newEmbeddableElement, newEmbeddableElement,
newFrameElement, newFrameElement,
@@ -49,7 +38,7 @@ import type App from "../../components/App";
import { createTestHook } from "../../components/App"; import { createTestHook } from "../../components/App";
import type { Action } from "../../actions/types"; import type { Action } from "../../actions/types";
import { mutateElement } from "../../element/mutateElement"; import { mutateElement } from "../../element/mutateElement";
import { pointFrom, type LocalPoint, type Radians } from "../../../math"; import { point, type LocalPoint, type Radians } from "../../../math";
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
@@ -58,19 +47,6 @@ createTestHook();
const { h } = window; const { h } = window;
export class API { export class API {
static addSubtype = (record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) => {
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
if (checkRefreshOnSubtypeLoad(hasSubtype, h.elements)) {
h.app.refresh();
}
};
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
if (prep.actions) {
h.app.actionManager.registerAll(prep.actions);
}
return prep;
};
static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => { static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => {
act(() => { act(() => {
h.app.updateScene(...args); h.app.updateScene(...args);
@@ -199,8 +175,6 @@ export class API {
verticalAlign?: T extends "text" verticalAlign?: T extends "text"
? ExcalidrawTextElement["verticalAlign"] ? ExcalidrawTextElement["verticalAlign"]
: never; : never;
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"];
boundElements?: ExcalidrawGenericElement["boundElements"]; boundElements?: ExcalidrawGenericElement["boundElements"];
containerId?: T extends "text" containerId?: T extends "text"
? ExcalidrawTextElement["containerId"] ? ExcalidrawTextElement["containerId"]
@@ -240,14 +214,6 @@ export class API {
const appState = h?.state || getDefaultAppState(); const appState = h?.state || getDefaultAppState();
const custom = maybeGetSubtypeProps(
{
subtype: rest.subtype ?? selectSubtype(appState, type)?.subtype,
customData:
rest.customData ?? selectSubtype(appState, type)?.customData,
},
type,
);
const base: Omit< const base: Omit<
ExcalidrawGenericElement, ExcalidrawGenericElement,
| "id" | "id"
@@ -262,7 +228,6 @@ export class API {
| "link" | "link"
| "updated" | "updated"
> = { > = {
...custom,
x, x,
y, y,
frameId: rest.frameId ?? null, frameId: rest.frameId ?? null,
@@ -342,8 +307,8 @@ export class API {
height, height,
type, type,
points: rest.points ?? [ points: rest.points ?? [
pointFrom<LocalPoint>(0, 0), point<LocalPoint>(0, 0),
pointFrom<LocalPoint>(100, 100), point<LocalPoint>(100, 100),
], ],
elbowed: rest.elbowed ?? false, elbowed: rest.elbowed ?? false,
}); });
@@ -355,8 +320,8 @@ export class API {
height, height,
type, type,
points: rest.points ?? [ points: rest.points ?? [
pointFrom<LocalPoint>(0, 0), point<LocalPoint>(0, 0),
pointFrom<LocalPoint>(100, 100), point<LocalPoint>(100, 100),
], ],
}); });
break; break;
@@ -1,7 +0,0 @@
{
"toolBar": {
"test": "Test",
"test2": "Test 2",
"test3": "Test 3"
}
}
+6 -9
View File
@@ -34,7 +34,7 @@ import { getTextEditor } from "../queries/dom";
import { arrayToMap } from "../../utils"; import { arrayToMap } from "../../utils";
import { createTestHook } from "../../components/App"; import { createTestHook } from "../../components/App";
import type { GlobalPoint, LocalPoint, Radians } from "../../../math"; import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
import { pointFrom, pointRotateRads } from "../../../math"; import { point, pointRotateRads } from "../../../math";
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
createTestHook(); createTestHook();
@@ -142,7 +142,7 @@ const getElementPointForSelection = (
element: ExcalidrawElement, element: ExcalidrawElement,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, height, angle } = element;
const target = pointFrom<GlobalPoint>( const target = point<GlobalPoint>(
x + x +
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2), (isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
y, y,
@@ -151,12 +151,9 @@ const getElementPointForSelection = (
if (isLinearElement(element)) { if (isLinearElement(element)) {
const bounds = getElementPointsCoords(element, element.points); const bounds = getElementPointsCoords(element, element.points);
center = pointFrom( center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2);
(bounds[0] + bounds[2]) / 2,
(bounds[1] + bounds[3]) / 2,
);
} else { } else {
center = pointFrom(x + width / 2, y + height / 2); center = point(x + width / 2, y + height / 2);
} }
if (isTextElement(element)) { if (isTextElement(element)) {
@@ -472,8 +469,8 @@ export class UI {
const width = initialWidth ?? initialHeight ?? size; const width = initialWidth ?? initialHeight ?? size;
const height = initialHeight ?? size; const height = initialHeight ?? size;
const points: LocalPoint[] = initialPoints ?? [ const points: LocalPoint[] = initialPoints ?? [
pointFrom(0, 0), point(0, 0),
pointFrom(width, height), point(width, height),
]; ];
UI.clickTool(type); UI.clickTool(type);
+9 -9
View File
@@ -46,7 +46,7 @@ import { HistoryEntry } from "../history";
import { AppStateChange, ElementsChange } from "../change"; import { AppStateChange, ElementsChange } from "../change";
import { Snapshot, StoreAction } from "../store"; import { Snapshot, StoreAction } from "../store";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { pointFrom } from "../../math"; import { point } from "../../math";
const { h } = window; const { h } = window;
@@ -2041,9 +2041,9 @@ describe("history", () => {
width: 178.9000000000001, width: 178.9000000000001,
height: 236.10000000000002, height: 236.10000000000002,
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(178.9000000000001, 0), point(178.9000000000001, 0),
pointFrom(178.9000000000001, 236.10000000000002), point(178.9000000000001, 236.10000000000002),
], ],
startBinding: { startBinding: {
elementId: "KPrBI4g_v9qUB1XxYLgSz", elementId: "KPrBI4g_v9qUB1XxYLgSz",
@@ -2159,11 +2159,11 @@ describe("history", () => {
elements: [ elements: [
newElementWith(h.elements[0] as ExcalidrawLinearElement, { newElementWith(h.elements[0] as ExcalidrawLinearElement, {
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(5, 5), point(5, 5),
pointFrom(10, 10), point(10, 10),
pointFrom(15, 15), point(15, 15),
pointFrom(20, 20), point(20, 20),
] as LocalPoint[], ] as LocalPoint[],
}), }),
], ],
@@ -28,7 +28,7 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { vi } from "vitest"; import { vi } from "vitest";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import type { GlobalPoint } from "../../math"; import type { GlobalPoint } from "../../math";
import { pointCenter, pointFrom } from "../../math"; import { pointCenter, point } from "../../math";
const renderInteractiveScene = vi.spyOn( const renderInteractiveScene = vi.spyOn(
InteractiveCanvas, InteractiveCanvas,
@@ -57,8 +57,8 @@ describe("Test Linear Elements", () => {
interactiveCanvas = container.querySelector("canvas.interactive")!; interactiveCanvas = container.querySelector("canvas.interactive")!;
}); });
const p1 = pointFrom<GlobalPoint>(20, 20); const p1 = point<GlobalPoint>(20, 20);
const p2 = pointFrom<GlobalPoint>(60, 20); const p2 = point<GlobalPoint>(60, 20);
const midpoint = pointCenter<GlobalPoint>(p1, p2); const midpoint = pointCenter<GlobalPoint>(p1, p2);
const delta = 50; const delta = 50;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -75,7 +75,7 @@ describe("Test Linear Elements", () => {
height: 0, height: 0,
type, type,
roughness, roughness,
points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])], points: [point(0, 0), point(p2[0] - p1[0], p2[1] - p1[1])],
roundness, roundness,
}); });
API.setElements([line]); API.setElements([line]);
@@ -99,9 +99,9 @@ describe("Test Linear Elements", () => {
type, type,
roughness, roughness,
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(p3[0], p3[1]), point(p3[0], p3[1]),
pointFrom(p2[0] - p1[0], p2[1] - p1[1]), point(p2[0] - p1[0], p2[1] - p1[1]),
], ],
roundness, roundness,
}); });
@@ -161,7 +161,7 @@ describe("Test Linear Elements", () => {
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
@@ -169,7 +169,7 @@ describe("Test Linear Elements", () => {
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
expect(line.x).toBe(originalX); expect(line.x).toBe(originalX);
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -184,7 +184,7 @@ describe("Test Linear Elements", () => {
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
// drag line from midpoint // drag line from midpoint
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -248,7 +248,7 @@ describe("Test Linear Elements", () => {
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
expect(line.x).toBe(originalX); expect(line.x).toBe(originalX);
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -261,7 +261,7 @@ describe("Test Linear Elements", () => {
enterLineEditingMode(line); enterLineEditingMode(line);
// drag line from midpoint // drag line from midpoint
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
); );
@@ -356,7 +356,7 @@ describe("Test Linear Elements", () => {
const startPoint = pointCenter(points[0], midPoints[0]!); const startPoint = pointCenter(points[0], midPoints[0]!);
const deltaX = 50; const deltaX = 50;
const deltaY = 20; const deltaY = 20;
const endPoint = pointFrom<GlobalPoint>( const endPoint = point<GlobalPoint>(
startPoint[0] + deltaX, startPoint[0] + deltaX,
startPoint[1] + deltaY, startPoint[1] + deltaY,
); );
@@ -399,8 +399,8 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = pointFrom<GlobalPoint>(55, 45); const firstSegmentMidpoint = point<GlobalPoint>(55, 45);
const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40); const lastSegmentMidpoint = point<GlobalPoint>(75, 40);
let line: ExcalidrawLinearElement; let line: ExcalidrawLinearElement;
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
// drag line via first segment midpoint // drag line via first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
pointFrom( point(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -426,10 +426,7 @@ describe("Test Linear Elements", () => {
// drag line from last segment midpoint // drag line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
pointFrom( point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
@@ -478,10 +475,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -519,10 +516,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -559,7 +556,7 @@ describe("Test Linear Elements", () => {
// dragging line from last segment midpoint // dragging line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50), point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
); );
expect(line.points.length).toEqual(4); expect(line.points.length).toEqual(4);
@@ -592,11 +589,11 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = pointFrom<GlobalPoint>( const firstSegmentMidpoint = point<GlobalPoint>(
55.9697848965255, 55.9697848965255,
47.442326230998205, 47.442326230998205,
); );
const lastSegmentMidpoint = pointFrom<GlobalPoint>( const lastSegmentMidpoint = point<GlobalPoint>(
76.08587175006699, 76.08587175006699,
43.294165939653226, 43.294165939653226,
); );
@@ -615,7 +612,7 @@ describe("Test Linear Elements", () => {
// drag line from first segment midpoint // drag line from first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
pointFrom( point(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -625,10 +622,7 @@ describe("Test Linear Elements", () => {
// drag line from last segment midpoint // drag line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
pointFrom( point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`16`, `16`,
@@ -675,10 +669,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
const newPoints = LinearElementEditor.getPointsGlobalCoordinates( const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line, line,
@@ -723,10 +717,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -757,10 +751,7 @@ describe("Test Linear Elements", () => {
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
pointFrom( point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(line.points.length).toEqual(4); expect(line.points.length).toEqual(4);
@@ -820,8 +811,8 @@ describe("Test Linear Elements", () => {
API.setSelectedElements([line]); API.setSelectedElements([line]);
enterLineEditingMode(line, true); enterLineEditingMode(line, true);
drag( drag(
pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y), point(line.points[0][0] + line.x, line.points[0][1] + line.y),
pointFrom( point(
dragEndPositionOffset[0] + line.x, dragEndPositionOffset[0] + line.x,
dragEndPositionOffset[1] + line.y, dragEndPositionOffset[1] + line.y,
), ),
@@ -936,14 +927,14 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = pointFrom<GlobalPoint>( const firstSegmentMidpoint = point<GlobalPoint>(
55.9697848965255, 55.9697848965255,
47.442326230998205, 47.442326230998205,
); );
// drag line from first segment midpoint // drag line from first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
pointFrom( point(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -1160,7 +1151,7 @@ describe("Test Linear Elements", () => {
); );
// Drag from last point // Drag from last point
drag(points[1], pointFrom(points[1][0] + 300, points[1][1])); drag(points[1], point(points[1][0] + 300, points[1][1]));
expect({ width: container.width, height: container.height }) expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
@@ -1359,11 +1350,11 @@ describe("Test Linear Elements", () => {
[ [
{ {
index: 0, index: 0,
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10), point: point(line.points[0][0] + 10, line.points[0][1] + 10),
}, },
{ {
index: line.points.length - 1, index: line.points.length - 1,
point: pointFrom( point: point(
line.points[line.points.length - 1][0] - 10, line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10, line.points[line.points.length - 1][1] - 10,
), ),
+30 -35
View File
@@ -17,7 +17,7 @@ import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom } from "../../math"; import { point } from "../../math";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -220,17 +220,12 @@ describe("generic element", () => {
describe.each(["line", "freedraw"] as const)("%s element", (type) => { describe.each(["line", "freedraw"] as const)("%s element", (type) => {
const points: Record<typeof type, LocalPoint[]> = { const points: Record<typeof type, LocalPoint[]> = {
line: [ line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)],
pointFrom(0, 0),
pointFrom(60, -20),
pointFrom(20, 40),
pointFrom(-40, 0),
],
freedraw: [ freedraw: [
pointFrom(0, 0), point(0, 0),
pointFrom(-2.474600807561444, 41.021700699972), point(-2.474600807561444, 41.021700699972),
pointFrom(3.6627956000014024, 47.84174560617245), point(3.6627956000014024, 47.84174560617245),
pointFrom(40.495224145598115, 47.15909710753482), point(40.495224145598115, 47.15909710753482),
], ],
}; };
@@ -298,11 +293,11 @@ describe("arrow element", () => {
it("resizes with a label", async () => { it("resizes with a label", async () => {
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(40, 140), point(40, 140),
pointFrom(80, 60), // label's anchor point(80, 60), // label's anchor
pointFrom(180, 20), point(180, 20),
pointFrom(200, 120), point(200, 120),
], ],
}); });
const label = await UI.editText(arrow, "Hello"); const label = await UI.editText(arrow, "Hello");
@@ -752,24 +747,24 @@ describe("multiple selection", () => {
x: 60, x: 60,
y: 40, y: 40,
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(-40, 40), point(-40, 40),
pointFrom(-60, 0), point(-60, 0),
pointFrom(0, -40), point(0, -40),
pointFrom(40, 20), point(40, 20),
pointFrom(0, 40), point(0, 40),
], ],
}); });
const freedraw = UI.createElement("freedraw", { const freedraw = UI.createElement("freedraw", {
x: 63.56072661326618, x: 63.56072661326618,
y: 100, y: 100,
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(-43.56072661326618, 18.15048126846341), point(-43.56072661326618, 18.15048126846341),
pointFrom(-43.56072661326618, 29.041198460587566), point(-43.56072661326618, 29.041198460587566),
pointFrom(-38.115368017204105, 42.652452795512204), point(-38.115368017204105, 42.652452795512204),
pointFrom(-19.964886748740696, 66.24829266003775), point(-19.964886748740696, 66.24829266003775),
pointFrom(19.056612930986716, 77.1390098521619), point(19.056612930986716, 77.1390098521619),
], ],
}); });
@@ -1106,13 +1101,13 @@ describe("multiple selection", () => {
x: 60, x: 60,
y: 0, y: 0,
points: [ points: [
pointFrom(0, 0), point(0, 0),
pointFrom(-40, 40), point(-40, 40),
pointFrom(-20, 60), point(-20, 60),
pointFrom(20, 20), point(20, 20),
pointFrom(40, 40), point(40, 40),
pointFrom(-20, 100), point(-20, 100),
pointFrom(-60, 60), point(-60, 60),
], ],
}); });
File diff suppressed because one or more lines are too long
-679
View File
@@ -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);
});
});
-14
View File
@@ -35,12 +35,6 @@ import type { ClipboardData } from "./clipboard";
import type { isOverScrollBars } from "./scene/scrollbars"; import type { isOverScrollBars } from "./scene/scrollbars";
import type { MaybeTransformHandleType } from "./element/transformHandles"; import type { MaybeTransformHandleType } from "./element/transformHandles";
import type Library from "./data/library"; import type Library from "./data/library";
import type {
SubtypeMethods,
Subtype,
SubtypePrepFn,
SubtypeRecord,
} from "./element/subtypes";
import type { FileSystemHandle } from "./data/filesystem"; import type { FileSystemHandle } from "./data/filesystem";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { ContextMenuItems } from "./components/ContextMenu"; import type { ContextMenuItems } from "./components/ContextMenu";
@@ -277,10 +271,6 @@ export interface AppState {
*/ */
editingTextElement: NonDeletedExcalidrawElement | null; editingTextElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null; editingLinearElement: LinearElementEditor | null;
activeSubtypes?: Subtype[];
customData?: {
[subtype: Subtype]: ExcalidrawElement["customData"];
};
activeTool: { activeTool: {
/** /**
* indicates a previous tool we should revert back to if we deselect the * indicates a previous tool we should revert back to if we deselect the
@@ -749,10 +739,6 @@ export interface ExcalidrawImperativeAPI {
getName: InstanceType<typeof App>["getName"]; getName: InstanceType<typeof App>["getName"];
scrollToContent: InstanceType<typeof App>["scrollToContent"]; scrollToContent: InstanceType<typeof App>["scrollToContent"];
registerAction: (action: Action) => void; registerAction: (action: Action) => void;
addSubtype: (
record: SubtypeRecord,
subtypePrepFn: SubtypePrepFn,
) => { actions: readonly Action[] | null; methods: Partial<SubtypeMethods> };
refresh: InstanceType<typeof App>["refresh"]; refresh: InstanceType<typeof App>["refresh"];
setToast: InstanceType<typeof App>["setToast"]; setToast: InstanceType<typeof App>["setToast"];
addFiles: (data: BinaryFileData[]) => void; addFiles: (data: BinaryFileData[]) => void;
+21 -26
View File
@@ -1,9 +1,4 @@
import { import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
isLineSegment,
lineSegment,
pointFrom,
type GlobalPoint,
} from "../math";
import type { LineSegment } from "../utils"; import type { LineSegment } from "../utils";
import type { BoundingBox, Bounds } from "./element/bounds"; import type { BoundingBox, Bounds } from "./element/bounds";
import { isBounds } from "./element/typeChecks"; import { isBounds } from "./element/typeChecks";
@@ -57,8 +52,8 @@ export const debugDrawPoint = (
debugDrawLine( debugDrawLine(
lineSegment( lineSegment(
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10), point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10), point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
), ),
{ {
color: opts?.color ?? "cyan", color: opts?.color ?? "cyan",
@@ -67,8 +62,8 @@ export const debugDrawPoint = (
); );
debugDrawLine( debugDrawLine(
lineSegment( lineSegment(
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10), point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10), point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
), ),
{ {
color: opts?.color ?? "cyan", color: opts?.color ?? "cyan",
@@ -88,20 +83,20 @@ export const debugDrawBoundingBox = (
debugDrawLine( debugDrawLine(
[ [
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox.minX, bbox.minY), point<GlobalPoint>(bbox.minX, bbox.minY),
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY), point<GlobalPoint>(bbox.maxX, bbox.minY),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY), point<GlobalPoint>(bbox.maxX, bbox.minY),
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY), point<GlobalPoint>(bbox.maxX, bbox.maxY),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY), point<GlobalPoint>(bbox.maxX, bbox.maxY),
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY), point<GlobalPoint>(bbox.minX, bbox.maxY),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY), point<GlobalPoint>(bbox.minX, bbox.maxY),
pointFrom<GlobalPoint>(bbox.minX, bbox.minY), point<GlobalPoint>(bbox.minX, bbox.minY),
), ),
], ],
{ {
@@ -123,20 +118,20 @@ export const debugDrawBounds = (
debugDrawLine( debugDrawLine(
[ [
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox[0], bbox[1]), point<GlobalPoint>(bbox[0], bbox[1]),
pointFrom<GlobalPoint>(bbox[2], bbox[1]), point<GlobalPoint>(bbox[2], bbox[1]),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox[2], bbox[1]), point<GlobalPoint>(bbox[2], bbox[1]),
pointFrom<GlobalPoint>(bbox[2], bbox[3]), point<GlobalPoint>(bbox[2], bbox[3]),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox[2], bbox[3]), point<GlobalPoint>(bbox[2], bbox[3]),
pointFrom<GlobalPoint>(bbox[0], bbox[3]), point<GlobalPoint>(bbox[0], bbox[3]),
), ),
lineSegment( lineSegment(
pointFrom<GlobalPoint>(bbox[0], bbox[3]), point<GlobalPoint>(bbox[0], bbox[3]),
pointFrom<GlobalPoint>(bbox[0], bbox[1]), point<GlobalPoint>(bbox[0], bbox[1]),
), ),
], ],
{ {
+4 -4
View File
@@ -1,5 +1,5 @@
import { isPointOnSymmetricArc } from "./arc"; import { isPointOnSymmetricArc } from "./arc";
import { pointFrom } from "./point"; import { point } from "./point";
describe("point on arc", () => { describe("point on arc", () => {
it("should detect point on simple arc", () => { it("should detect point on simple arc", () => {
@@ -10,7 +10,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
pointFrom(0.92291667, 0.385), point(0.92291667, 0.385),
), ),
).toBe(true); ).toBe(true);
}); });
@@ -22,7 +22,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
pointFrom(-0.92291667, 0.385), point(-0.92291667, 0.385),
), ),
).toBe(false); ).toBe(false);
}); });
@@ -34,7 +34,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
pointFrom(-0.5, 0.5), point(-0.5, 0.5),
), ),
).toBe(false); ).toBe(false);
}); });
+11 -11
View File
@@ -1,4 +1,4 @@
import { pointFrom, pointRotateRads } from "./point"; import { point, pointRotateRads } from "./point";
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types"; import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
/** /**
@@ -43,10 +43,10 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
const out: Point[] = []; const out: Point[] = [];
if (len === 3) { if (len === 3) {
out.push( out.push(
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
); );
} else { } else {
const points: Point[] = []; const points: Point[] = [];
@@ -59,19 +59,19 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
} }
const b: Point[] = []; const b: Point[] = [];
const s = 1 - curveTightness; const s = 1 - curveTightness;
out.push(pointFrom(points[0][0], points[0][1])); out.push(point(points[0][0], points[0][1]));
for (let i = 1; i + 2 < points.length; i++) { for (let i = 1; i + 2 < points.length; i++) {
const cachedVertArray = points[i]; const cachedVertArray = points[i];
b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]); b[0] = point(cachedVertArray[0], cachedVertArray[1]);
b[1] = pointFrom( b[1] = point(
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
); );
b[2] = pointFrom( b[2] = point(
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
); );
b[3] = pointFrom(points[i + 1][0], points[i + 1][1]); b[3] = point(points[i + 1][0], points[i + 1][1]);
out.push(b[1], b[2], b[3]); out.push(b[1], b[2], b[3]);
} }
} }
@@ -102,7 +102,7 @@ export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
3 * (1 - t) * Math.pow(t, 2) * p2[1] + 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
Math.pow(t, 3) * p3[1]; Math.pow(t, 3) * p3[1];
return pointFrom(x, y); return point(x, y);
}; };
/** /**
+5 -5
View File
@@ -1,4 +1,4 @@
import { pointFrom, pointRotateRads } from "./point"; import { point, pointRotateRads } from "./point";
import type { Radians } from "./types"; import type { Radians } from "./types";
describe("rotate", () => { describe("rotate", () => {
@@ -9,14 +9,14 @@ describe("rotate", () => {
const y2 = 30; const y2 = 30;
const angle = (Math.PI / 2) as Radians; const angle = (Math.PI / 2) as Radians;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
pointFrom(x1, y1), point(x1, y1),
pointFrom(x2, y2), point(x2, y2),
angle, angle,
); );
expect([rotatedX, rotatedY]).toEqual([30, 20]); expect([rotatedX, rotatedY]).toEqual([30, 20]);
const res2 = pointRotateRads( const res2 = pointRotateRads(
pointFrom(rotatedX, rotatedY), point(rotatedX, rotatedY),
pointFrom(x2, y2), point(x2, y2),
-angle as Radians, -angle as Radians,
); );
expect(res2).toEqual([x1, x2]); expect(res2).toEqual([x1, x2]);
+7 -7
View File
@@ -16,7 +16,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
* @param y The Y coordinate * @param y The Y coordinate
* @returns The branded and created point * @returns The branded and created point
*/ */
export function pointFrom<Point extends GlobalPoint | LocalPoint>( export function point<Point extends GlobalPoint | LocalPoint>(
x: number, x: number,
y: number, y: number,
): Point { ): Point {
@@ -33,7 +33,7 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
numberArray: number[], numberArray: number[],
): Point | undefined { ): Point | undefined {
return numberArray.length === 2 return numberArray.length === 2
? pointFrom<Point>(numberArray[0], numberArray[1]) ? point<Point>(numberArray[0], numberArray[1])
: undefined; : undefined;
} }
@@ -107,7 +107,7 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
[cx, cy]: Point, [cx, cy]: Point,
angle: Radians, angle: Radians,
): Point { ): Point {
return pointFrom( return point(
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx, (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy, (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
); );
@@ -146,7 +146,7 @@ export function pointTranslate<
From extends GlobalPoint | LocalPoint, From extends GlobalPoint | LocalPoint,
To extends GlobalPoint | LocalPoint, To extends GlobalPoint | LocalPoint,
>(p: From, v: Vector = [0, 0] as Vector): To { >(p: From, v: Vector = [0, 0] as Vector): To {
return pointFrom(p[0] + v[0], p[1] + v[1]); return point(p[0] + v[0], p[1] + v[1]);
} }
/** /**
@@ -157,7 +157,7 @@ export function pointTranslate<
* @returns The middle point * @returns The middle point
*/ */
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P { export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
} }
/** /**
@@ -172,7 +172,7 @@ export function pointAdd<Point extends LocalPoint | GlobalPoint>(
a: Point, a: Point,
b: Point, b: Point,
): Point { ): Point {
return pointFrom(a[0] + b[0], a[1] + b[1]); return point(a[0] + b[0], a[1] + b[1]);
} }
/** /**
@@ -187,7 +187,7 @@ export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
a: Point, a: Point,
b: Point, b: Point,
): Point { ): Point {
return pointFrom(a[0] - b[0], a[1] - b[1]); return point(a[0] - b[0], a[1] - b[1]);
} }
/** /**
+33 -33
View File
@@ -4,7 +4,7 @@ import {
degreesToRadians, degreesToRadians,
lineSegment, lineSegment,
lineSegmentRotate, lineSegmentRotate,
pointFrom, point,
pointRotateDegs, pointRotateDegs,
} from "../math"; } from "../math";
import { pointOnCurve, pointOnPolyline } from "./collision"; import { pointOnCurve, pointOnPolyline } from "./collision";
@@ -12,21 +12,21 @@ import type { Polyline } from "./geometry/shape";
describe("point and curve", () => { describe("point and curve", () => {
const c: Curve<GlobalPoint> = curve( const c: Curve<GlobalPoint> = curve(
pointFrom(1.4, 1.65), point(1.4, 1.65),
pointFrom(1.9, 7.9), point(1.9, 7.9),
pointFrom(5.9, 1.65), point(5.9, 1.65),
pointFrom(6.44, 4.84), point(6.44, 4.84),
); );
it("point on curve", () => { it("point on curve", () => {
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true); expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true); expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true); expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true); expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true); expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false); expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false);
expect(pointOnCurve(c[1], c, 0.1)).toBe(false); expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
expect(pointOnCurve(c[2], c, 0.1)).toBe(false); expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
}); });
@@ -34,52 +34,52 @@ describe("point and curve", () => {
describe("point and polylines", () => { describe("point and polylines", () => {
const polyline: Polyline<GlobalPoint> = [ const polyline: Polyline<GlobalPoint> = [
lineSegment(pointFrom(1, 0), pointFrom(1, 2)), lineSegment(point(1, 0), point(1, 2)),
lineSegment(pointFrom(1, 2), pointFrom(2, 2)), lineSegment(point(1, 2), point(2, 2)),
lineSegment(pointFrom(2, 2), pointFrom(2, 1)), lineSegment(point(2, 2), point(2, 1)),
lineSegment(pointFrom(2, 1), pointFrom(3, 1)), lineSegment(point(2, 1), point(3, 1)),
]; ];
it("point on the line", () => { it("point on the line", () => {
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true); expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true); expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true); expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true); expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true); expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true); expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true); expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true); expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false); expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false); expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false);
}); });
it("point on the line with rotation", () => { it("point on the line with rotation", () => {
const truePoints = [ const truePoints = [
pointFrom(1, 0), point(1, 0),
pointFrom(1, 2), point(1, 2),
pointFrom(2, 2), point(2, 2),
pointFrom(2, 1), point(2, 1),
pointFrom(3, 1), point(3, 1),
]; ];
truePoints.forEach((p) => { truePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
}); });
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)]; const falsePoints = [point(0, 1), point(2.1, 1.5)];
falsePoints.forEach((p) => { falsePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
}); });
+2 -2
View File
@@ -7,7 +7,7 @@ import {
import type { Curve } from "../math"; import type { Curve } from "../math";
import { import {
lineSegment, lineSegment,
pointFrom, point,
polygonIncludesPoint, polygonIncludesPoint,
pointOnLineSegment, pointOnLineSegment,
pointOnPolygon, pointOnPolygon,
@@ -110,7 +110,7 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
for (let i = 0; i < segments; i++) { for (let i = 0; i < segments; i++) {
t += increment; t += increment;
if (t <= 1) { if (t <= 1) {
const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1)); const nextPoint: Point = point(equation(t, 0), equation(t, 1));
lineSegments.push(lineSegment(startingPoint, nextPoint)); lineSegments.push(lineSegment(startingPoint, nextPoint));
startingPoint = nextPoint; startingPoint = nextPoint;
} }
+45 -79
View File
@@ -1,6 +1,6 @@
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math"; import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
import { import {
pointFrom, point,
lineSegment, lineSegment,
polygon, polygon,
pointOnLineSegment, pointOnLineSegment,
@@ -23,127 +23,93 @@ describe("point and line", () => {
// expect(pointRightofLine(point(2, 1), l)).toBe(true); // expect(pointRightofLine(point(2, 1), l)).toBe(true);
// }); // });
const s: LineSegment<GlobalPoint> = lineSegment( const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
pointFrom(1, 0),
pointFrom(1, 2),
);
it("point on the line", () => { it("point on the line", () => {
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false); expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true); expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false); expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
}); });
}); });
describe("point and polygon", () => { describe("point and polygon", () => {
const poly: Polygon<GlobalPoint> = polygon( const poly: Polygon<GlobalPoint> = polygon(
pointFrom(10, 10), point(10, 10),
pointFrom(50, 10), point(50, 10),
pointFrom(50, 50), point(50, 50),
pointFrom(10, 50), point(10, 50),
); );
it("point on polygon", () => { it("point on polygon", () => {
expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true); expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true); expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true); expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true); expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false); expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false); expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
}); });
it("point in polygon", () => { it("point in polygon", () => {
const poly: Polygon<GlobalPoint> = polygon( const poly: Polygon<GlobalPoint> = polygon(
pointFrom(0, 0), point(0, 0),
pointFrom(2, 0), point(2, 0),
pointFrom(2, 2), point(2, 2),
pointFrom(0, 2), point(0, 2),
); );
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true); expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false); expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
}); });
}); });
describe("point and ellipse", () => { describe("point and ellipse", () => {
const ellipse: Ellipse<GlobalPoint> = { const ellipse: Ellipse<GlobalPoint> = {
center: pointFrom(0, 0), center: point(0, 0),
angle: 0 as Radians, angle: 0 as Radians,
halfWidth: 2, halfWidth: 2,
halfHeight: 1, halfHeight: 1,
}; };
it("point on ellipse", () => { it("point on ellipse", () => {
[ [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointOnEllipse(p, ellipse)).toBe(true); expect(pointOnEllipse(p, ellipse)).toBe(true);
}); });
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false); expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false); expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
}); });
it("point in ellipse", () => { it("point in ellipse", () => {
[ [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointInEllipse(p, ellipse)).toBe(true); expect(pointInEllipse(p, ellipse)).toBe(true);
}); });
expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true); expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true); expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false); expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false); expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
}); });
}); });
describe("line and line", () => { describe("line and line", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment( const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
pointFrom(1, 4), const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
pointFrom(3, 4), const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
); const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineB: LineSegment<GlobalPoint> = lineSegment( const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
pointFrom(2, 1), const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
pointFrom(2, 7), const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
);
const lineC: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineD: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineE: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 9),
pointFrom(3, 9),
);
const lineF: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 2),
pointFrom(3, 4),
);
const lineG: LineSegment<GlobalPoint> = lineSegment(
pointFrom(0, 1),
pointFrom(2, 3),
);
it("intersection", () => { it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]); expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);

Some files were not shown because too many files have changed in this diff Show More