From 6ff56c36e3a19194d9990beaeab6bcac76fbbbc2 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 6 Sep 2024 16:41:37 +0530 Subject: [PATCH 01/26] fix: add partial mocking (#8473) * fix: add partial mocking * lint * Update packages/utils/export.test.ts --- packages/excalidraw/tests/flip.test.tsx | 17 ++++++++--------- packages/utils/export.test.ts | 1 - 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index 5cf4cd55c7..53cbc53c8b 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -23,7 +23,6 @@ import { Excalidraw } from "../index"; import type { NormalizedZoomValue } from "../types"; import { ROUNDNESS } from "../constants"; import { vi } from "vitest"; -import * as blob from "../data/blob"; import { KEYS } from "../keys"; import { getBoundTextElementPosition } from "../element/textElement"; import { createPasteEvent } from "../clipboard"; @@ -33,15 +32,15 @@ import { point, type Radians } from "../../math"; const { h } = window; const mouse = new Pointer("mouse"); -// This needs to fixed in vitest mock, as when importActual used with mock -// the tests hangs - https://github.com/vitest-dev/vitest/issues/546. -// But fortunately spying and mocking the return value of spy works :p -const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile"); -const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile"); - -resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile); -generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId); +vi.mock("../data/blob", async (actual) => { + const orig: Object = await actual(); + return { + ...orig, + resizeImageFile: (imageFile: File) => imageFile, + generateIdFromFile: () => "fileId" as FileId, + }; +}); beforeEach(async () => { // Unmount ReactDOM from root diff --git a/packages/utils/export.test.ts b/packages/utils/export.test.ts index aa1049cc12..b04ec44e24 100644 --- a/packages/utils/export.test.ts +++ b/packages/utils/export.test.ts @@ -32,7 +32,6 @@ describe("exportToCanvas", async () => { describe("exportToBlob", async () => { describe("mime type", () => { - // afterEach(vi.restoreAllMocks); it("should change image/jpg to image/jpeg", async () => { const blob = await utils.exportToBlob({ ...diagramFactory(), From 5a11c70714f7b743af8fd9c08258191afc752b8d Mon Sep 17 00:00:00 2001 From: Abhishek Mehandiratta <36722596+abhi12299@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:26:00 +0530 Subject: [PATCH 02/26] fix: image rendering issue when passed in `initialData` (#8471) --- packages/excalidraw/components/App.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 00c0a882a8..183df35ccb 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -2295,6 +2295,9 @@ class App extends React.Component { storeAction: StoreAction.UPDATE, }); + // clear the shape and image cache so that any images in initialData + // can be loaded fresh + this.clearImageShapeCache(); // FontFaceSet loadingdone event we listen on may not always // fire (looking at you Safari), so on init we manually load all // fonts and rerender scene text elements once done. This also @@ -2360,6 +2363,15 @@ class App extends React.Component { return false; }; + private clearImageShapeCache() { + this.scene.getNonDeletedElements().forEach((element) => { + if (isInitializedImageElement(element) && this.files[element.fileId]) { + this.imageCache.delete(element.fileId); + ShapeCache.delete(element); + } + }); + } + public async componentDidMount() { this.unmounted = false; this.excalidrawContainerValue.container = @@ -3674,15 +3686,7 @@ class App extends React.Component { this.files = { ...this.files, ...Object.fromEntries(filesMap) }; - this.scene.getNonDeletedElements().forEach((element) => { - if ( - isInitializedImageElement(element) && - filesMap.has(element.fileId) - ) { - this.imageCache.delete(element.fileId); - ShapeCache.delete(element); - } - }); + this.clearImageShapeCache(); this.scene.triggerUpdate(); this.addNewImagesToImageCache(); From 6959a363f0ba8d0e8b05442f4c8f3cc4a2e00ca6 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Mon, 9 Sep 2024 23:12:07 +0800 Subject: [PATCH 03/26] feat: canvas search (#8438) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- .eslintignore | 1 + excalidraw-app/components/AppMainMenu.tsx | 1 + .../actions/actionToggleSearchMenu.ts | 51 ++ packages/excalidraw/actions/index.ts | 2 + packages/excalidraw/actions/shortcuts.ts | 4 +- packages/excalidraw/actions/types.ts | 6 +- packages/excalidraw/appState.ts | 2 + packages/excalidraw/components/App.tsx | 28 +- .../CommandPalette/CommandPalette.tsx | 15 +- .../excalidraw/components/DefaultSidebar.tsx | 6 +- packages/excalidraw/components/HelpDialog.tsx | 4 + packages/excalidraw/components/HintViewer.tsx | 8 + packages/excalidraw/components/LayerUI.tsx | 28 +- .../excalidraw/components/SearchMenu.scss | 110 +++ packages/excalidraw/components/SearchMenu.tsx | 671 ++++++++++++++++++ .../excalidraw/components/SearchSidebar.tsx | 29 + packages/excalidraw/components/TextField.scss | 28 +- packages/excalidraw/components/TextField.tsx | 10 +- .../components/canvases/InteractiveCanvas.tsx | 1 + packages/excalidraw/components/icons.tsx | 8 + .../components/main-menu/DefaultItems.tsx | 24 +- packages/excalidraw/constants.ts | 5 + packages/excalidraw/css/theme.scss | 6 +- packages/excalidraw/element/textElement.ts | 7 +- packages/excalidraw/locales/en.json | 8 + .../excalidraw/renderer/interactiveScene.ts | 49 +- .../__snapshots__/contextmenu.test.tsx.snap | 17 + .../__snapshots__/excalidraw.test.tsx.snap | 49 ++ .../tests/__snapshots__/history.test.tsx.snap | 58 ++ .../regressionTests.test.tsx.snap | 52 ++ packages/excalidraw/tests/helpers/ui.ts | 20 +- packages/excalidraw/tests/queries/dom.ts | 2 +- packages/excalidraw/tests/search.test.tsx | 143 ++++ packages/excalidraw/types.ts | 17 + .../utils/__snapshots__/export.test.ts.snap | 1 + 35 files changed, 1424 insertions(+), 47 deletions(-) create mode 100644 packages/excalidraw/actions/actionToggleSearchMenu.ts create mode 100644 packages/excalidraw/components/SearchMenu.scss create mode 100644 packages/excalidraw/components/SearchMenu.tsx create mode 100644 packages/excalidraw/components/SearchSidebar.tsx create mode 100644 packages/excalidraw/tests/search.test.tsx diff --git a/.eslintignore b/.eslintignore index 8578fb7d4a..8b4f458dee 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ public/workbox packages/excalidraw/types examples/**/public dev-dist +coverage diff --git a/excalidraw-app/components/AppMainMenu.tsx b/excalidraw-app/components/AppMainMenu.tsx index 04bddedefc..fd8d779a71 100644 --- a/excalidraw-app/components/AppMainMenu.tsx +++ b/excalidraw-app/components/AppMainMenu.tsx @@ -31,6 +31,7 @@ export const AppMainMenu: React.FC<{ /> )} + diff --git a/packages/excalidraw/actions/actionToggleSearchMenu.ts b/packages/excalidraw/actions/actionToggleSearchMenu.ts new file mode 100644 index 0000000000..6072fd30c9 --- /dev/null +++ b/packages/excalidraw/actions/actionToggleSearchMenu.ts @@ -0,0 +1,51 @@ +import { KEYS } from "../keys"; +import { register } from "./register"; +import type { AppState } from "../types"; +import { searchIcon } from "../components/icons"; +import { StoreAction } from "../store"; +import { CLASSES, SEARCH_SIDEBAR } from "../constants"; + +export const actionToggleSearchMenu = register({ + name: "searchMenu", + icon: searchIcon, + keywords: ["search", "find"], + label: "search.title", + viewMode: true, + trackEvent: { + category: "search_menu", + action: "toggle", + predicate: (appState) => appState.gridModeEnabled, + }, + perform(elements, appState, _, app) { + if (appState.openSidebar?.name === SEARCH_SIDEBAR.name) { + const searchInput = + app.excalidrawContainerValue.container?.querySelector( + `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, + ); + + if (searchInput?.matches(":focus")) { + return { + appState: { ...appState, openSidebar: null }, + storeAction: StoreAction.NONE, + }; + } + + searchInput?.focus(); + return false; + } + + return { + appState: { + ...appState, + openSidebar: { name: SEARCH_SIDEBAR.name }, + openDialog: null, + }, + storeAction: StoreAction.NONE, + }; + }, + checked: (appState: AppState) => appState.gridModeEnabled, + predicate: (element, appState, props) => { + return props.gridModeEnabled === undefined; + }, + keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F, +}); diff --git a/packages/excalidraw/actions/index.ts b/packages/excalidraw/actions/index.ts index 0920604255..eff5de297b 100644 --- a/packages/excalidraw/actions/index.ts +++ b/packages/excalidraw/actions/index.ts @@ -86,3 +86,5 @@ export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionLink } from "./actionLink"; export { actionToggleElementLock } from "./actionElementLock"; export { actionToggleLinearEditor } from "./actionLinearEditor"; + +export { actionToggleSearchMenu } from "./actionToggleSearchMenu"; diff --git a/packages/excalidraw/actions/shortcuts.ts b/packages/excalidraw/actions/shortcuts.ts index a5c3bad66c..025d91037c 100644 --- a/packages/excalidraw/actions/shortcuts.ts +++ b/packages/excalidraw/actions/shortcuts.ts @@ -51,7 +51,8 @@ export type ShortcutName = > | "saveScene" | "imageExport" - | "commandPalette"; + | "commandPalette" + | "searchMenu"; const shortcutMap: Record = { toggleTheme: [getShortcutKey("Shift+Alt+D")], @@ -112,6 +113,7 @@ const shortcutMap: Record = { saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")], saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")], toggleShortcuts: [getShortcutKey("?")], + searchMenu: [getShortcutKey("CtrlOrCmd+F")], }; export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => { diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index 2d0275bb3a..15364d21f3 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -137,7 +137,8 @@ export type ActionName = | "wrapTextInContainer" | "commandPalette" | "autoResize" - | "elementStats"; + | "elementStats" + | "searchMenu"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; @@ -191,7 +192,8 @@ export interface Action { | "history" | "menu" | "collab" - | "hyperlink"; + | "hyperlink" + | "search_menu"; action?: string; predicate?: ( appState: Readonly, diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index faad34057b..cb80c6cd89 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -116,6 +116,7 @@ export const getDefaultAppState = (): Omit< objectsSnapModeEnabled: false, userToFollow: null, followedBy: new Set(), + searchMatches: [], }; }; @@ -236,6 +237,7 @@ const APP_STATE_STORAGE_CONF = (< objectsSnapModeEnabled: { browser: true, export: false, server: false }, userToFollow: { browser: false, export: false, server: false }, followedBy: { browser: false, export: false, server: false }, + searchMatches: { browser: false, export: false, server: false }, }); const _clearAppStateForStorage = < diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 183df35ccb..e067bba7d1 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -440,6 +440,7 @@ import { FlowChartNavigator, getLinkDirectionFromKey, } from "../element/flowchart"; +import { searchItemInFocusAtom } from "./SearchMenu"; import type { LocalPoint, Radians } from "../../math"; import { point, pointDistance, vector } from "../../math"; @@ -548,6 +549,7 @@ class App extends React.Component { public scene: Scene; public fonts: Fonts; public renderer: Renderer; + public visibleElements: readonly NonDeletedExcalidrawElement[]; private resizeObserver: ResizeObserver | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined; public library: AppClassProperties["library"]; @@ -555,7 +557,7 @@ class App extends React.Component { public id: string; private store: Store; private history: History; - private excalidrawContainerValue: { + public excalidrawContainerValue: { container: HTMLDivElement | null; id: string; }; @@ -682,6 +684,7 @@ class App extends React.Component { this.canvas = document.createElement("canvas"); this.rc = rough.canvas(this.canvas); this.renderer = new Renderer(this.scene); + this.visibleElements = []; this.store = new Store(); this.history = new History(); @@ -1480,6 +1483,7 @@ class App extends React.Component { newElementId: this.state.newElement?.id, pendingImageElementId: this.state.pendingImageElementId, }); + this.visibleElements = visibleElements; const allElementsMap = this.scene.getNonDeletedElementsMap(); @@ -3800,7 +3804,7 @@ class App extends React.Component { }, ); - private getEditorUIOffsets = (): { + public getEditorUIOffsets = (): { top: number; right: number; bottom: number; @@ -5973,6 +5977,16 @@ class App extends React.Component { this.maybeCleanupAfterMissingPointerUp(event.nativeEvent); this.maybeUnfollowRemoteUser(); + if (this.state.searchMatches) { + this.setState((state) => ({ + searchMatches: state.searchMatches.map((searchMatch) => ({ + ...searchMatch, + focus: false, + })), + })); + jotaiStore.set(searchItemInFocusAtom, null); + } + // since contextMenu options are potentially evaluated on each render, // and an contextMenu action may depend on selection state, we must // close the contextMenu before we update the selection on pointerDown @@ -6401,8 +6415,16 @@ class App extends React.Component { } isPanning = true; + // due to event.preventDefault below, container wouldn't get focus + // automatically + this.focusContainer(); + + // preventing defualt while text editing messes with cursor/focus if (!this.state.editingTextElement) { - // preventing defualt while text editing messes with cursor/focus + // necessary to prevent browser from scrolling the page if excalidraw + // not full-page #4489 + // + // as such, the above is broken when panning canvas while in wysiwyg event.preventDefault(); } diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx index 36c9a9a687..e732acfb56 100644 --- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx +++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx @@ -43,7 +43,11 @@ import { InlineIcon } from "../InlineIcon"; import { SHAPES } from "../../shapes"; import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions"; import { useStableCallback } from "../../hooks/useStableCallback"; -import { actionClearCanvas, actionLink } from "../../actions"; +import { + actionClearCanvas, + actionLink, + actionToggleSearchMenu, +} from "../../actions"; import { jotaiStore } from "../../jotai"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import type { CommandPaletteItem } from "./types"; @@ -382,6 +386,15 @@ function CommandPaletteInner({ } }, }, + { + label: t("search.title"), + category: DEFAULT_CATEGORIES.app, + icon: searchIcon, + viewMode: true, + perform: () => { + actionManager.executeAction(actionToggleSearchMenu); + }, + }, { label: t("labels.changeStroke"), keywords: ["color", "outline"], diff --git a/packages/excalidraw/components/DefaultSidebar.tsx b/packages/excalidraw/components/DefaultSidebar.tsx index 78b03007f8..5cd588933a 100644 --- a/packages/excalidraw/components/DefaultSidebar.tsx +++ b/packages/excalidraw/components/DefaultSidebar.tsx @@ -2,7 +2,6 @@ import clsx from "clsx"; import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants"; import { useTunnels } from "../context/tunnels"; import { useUIAppState } from "../context/ui-appState"; -import { t } from "../i18n"; import type { MarkOptional, Merge } from "../utility-types"; import { composeEventHandlers } from "../utils"; import { useExcalidrawSetAppState } from "./App"; @@ -10,6 +9,8 @@ import { withInternalFallback } from "./hoc/withInternalFallback"; import { LibraryMenu } from "./LibraryMenu"; import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common"; import { Sidebar } from "./Sidebar/Sidebar"; +import "../components/dropdownMenu/DropdownMenu.scss"; +import { t } from "../i18n"; const DefaultSidebarTrigger = withInternalFallback( "DefaultSidebarTrigger", @@ -68,8 +69,7 @@ export const DefaultSidebar = Object.assign( return ( void }) => { label={t("stats.fullTitle")} shortcuts={[getShortcutKey("Alt+/")]} /> + )} + @@ -362,16 +365,21 @@ const LayerUI = ({ const renderSidebars = () => { return ( - { - trackEvent( - "sidebar", - `toggleDock (${docked ? "dock" : "undock"})`, - `(${device.editor.isMobile ? "mobile" : "desktop"})`, - ); - }} - /> + <> + {appState.openSidebar?.name === SEARCH_SIDEBAR.name && ( + + )} + { + trackEvent( + "sidebar", + `toggleDock (${docked ? "dock" : "undock"})`, + `(${device.editor.isMobile ? "mobile" : "desktop"})`, + ); + }} + /> + ); }; diff --git a/packages/excalidraw/components/SearchMenu.scss b/packages/excalidraw/components/SearchMenu.scss new file mode 100644 index 0000000000..ae6bb5647e --- /dev/null +++ b/packages/excalidraw/components/SearchMenu.scss @@ -0,0 +1,110 @@ +@import "open-color/open-color"; + +.excalidraw { + .layer-ui__search { + flex: 1 0 auto; + display: flex; + flex-direction: column; + padding: 8px 0 0 0; + } + + .layer-ui__search-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.75rem; + .ExcTextField { + flex: 1 0 auto; + } + + .ExcTextField__input { + background-color: #f5f5f9; + @at-root .excalidraw.theme--dark#{&} { + background-color: #31303b; + } + + border-radius: var(--border-radius-md); + border: 0; + + input::placeholder { + font-size: 0.9rem; + } + } + } + + .layer-ui__search-count { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 8px 0 8px; + margin: 0 0.75rem 0.25rem 0.75rem; + font-size: 0.8em; + + .result-nav { + display: flex; + + .result-nav-btn { + width: 36px; + height: 36px; + --button-border: transparent; + + &:active { + background-color: var(--color-surface-high); + } + + &:first { + margin-right: 4px; + } + } + } + } + + .layer-ui__search-result-container { + overflow-y: auto; + flex: 1 1 0; + display: flex; + flex-direction: column; + + gap: 0.125rem; + } + + .layer-ui__result-item { + display: flex; + align-items: center; + min-height: 2rem; + flex: 0 0 auto; + padding: 0.25rem 0.75rem; + cursor: pointer; + border: 1px solid transparent; + outline: none; + + margin: 0 0.75rem; + border-radius: var(--border-radius-md); + + .text-icon { + width: 1rem; + height: 1rem; + margin-right: 0.75rem; + } + + .preview-text { + flex: 1; + max-height: 48px; + line-height: 24px; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + } + + &:hover { + background-color: var(--color-surface-high); + } + &:active { + border-color: var(--color-primary); + } + + &.active { + background-color: var(--color-surface-high); + } + } +} diff --git a/packages/excalidraw/components/SearchMenu.tsx b/packages/excalidraw/components/SearchMenu.tsx new file mode 100644 index 0000000000..f05660a52f --- /dev/null +++ b/packages/excalidraw/components/SearchMenu.tsx @@ -0,0 +1,671 @@ +import { Fragment, memo, useEffect, useRef, useState } from "react"; +import { collapseDownIcon, upIcon, searchIcon } from "./icons"; +import { TextField } from "./TextField"; +import { Button } from "./Button"; +import { useApp, useExcalidrawSetAppState } from "./App"; +import { debounce } from "lodash"; +import type { AppClassProperties } from "../types"; +import { isTextElement, newTextElement } from "../element"; +import type { ExcalidrawTextElement } from "../element/types"; +import { measureText } from "../element/textElement"; +import { addEventListener, getFontString } from "../utils"; +import { KEYS } from "../keys"; + +import "./SearchMenu.scss"; +import clsx from "clsx"; +import { atom, useAtom } from "jotai"; +import { jotaiScope } from "../jotai"; +import { t } from "../i18n"; +import { isElementCompletelyInViewport } from "../element/sizeHelpers"; +import { randomInteger } from "../random"; +import { CLASSES, EVENT } from "../constants"; +import { useStable } from "../hooks/useStable"; + +const searchKeywordAtom = atom(""); +export const searchItemInFocusAtom = atom(null); + +const SEARCH_DEBOUNCE = 350; + +type SearchMatchItem = { + textElement: ExcalidrawTextElement; + keyword: string; + index: number; + preview: { + indexInKeyword: number; + previewText: string; + moreBefore: boolean; + moreAfter: boolean; + }; + matchedLines: { + offsetX: number; + offsetY: number; + width: number; + height: number; + }[]; +}; + +type SearchMatches = { + nonce: number | null; + items: SearchMatchItem[]; +}; + +export const SearchMenu = () => { + const app = useApp(); + const setAppState = useExcalidrawSetAppState(); + + const searchInputRef = useRef(null); + + const [keyword, setKeyword] = useAtom(searchKeywordAtom, jotaiScope); + const [searchMatches, setSearchMatches] = useState({ + nonce: null, + items: [], + }); + const searchedKeywordRef = useRef(); + const lastSceneNonceRef = useRef(); + + const [focusIndex, setFocusIndex] = useAtom( + searchItemInFocusAtom, + jotaiScope, + ); + const elementsMap = app.scene.getNonDeletedElementsMap(); + + useEffect(() => { + const trimmedKeyword = keyword.trim(); + if ( + trimmedKeyword !== searchedKeywordRef.current || + app.scene.getSceneNonce() !== lastSceneNonceRef.current + ) { + searchedKeywordRef.current = null; + handleSearch(trimmedKeyword, app, (matchItems, index) => { + setSearchMatches({ + nonce: randomInteger(), + items: matchItems, + }); + setFocusIndex(index); + searchedKeywordRef.current = trimmedKeyword; + lastSceneNonceRef.current = app.scene.getSceneNonce(); + setAppState({ + searchMatches: matchItems.map((searchMatch) => ({ + id: searchMatch.textElement.id, + focus: false, + matchedLines: searchMatch.matchedLines, + })), + }); + }); + } + }, [ + keyword, + elementsMap, + app, + setAppState, + setFocusIndex, + lastSceneNonceRef, + ]); + + const goToNextItem = () => { + if (searchMatches.items.length > 0) { + setFocusIndex((focusIndex) => { + if (focusIndex === null) { + return 0; + } + + return (focusIndex + 1) % searchMatches.items.length; + }); + } + }; + + const goToPreviousItem = () => { + if (searchMatches.items.length > 0) { + setFocusIndex((focusIndex) => { + if (focusIndex === null) { + return 0; + } + + return focusIndex - 1 < 0 + ? searchMatches.items.length - 1 + : focusIndex - 1; + }); + } + }; + + useEffect(() => { + if (searchMatches.items.length > 0 && focusIndex !== null) { + const match = searchMatches.items[focusIndex]; + + if (match) { + const matchAsElement = newTextElement({ + text: match.keyword, + x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0), + y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0), + width: match.matchedLines[0]?.width, + height: match.matchedLines[0]?.height, + }); + + if ( + !isElementCompletelyInViewport( + [matchAsElement], + app.canvas.width / window.devicePixelRatio, + app.canvas.height / window.devicePixelRatio, + { + offsetLeft: app.state.offsetLeft, + offsetTop: app.state.offsetTop, + scrollX: app.state.scrollX, + scrollY: app.state.scrollY, + zoom: app.state.zoom, + }, + app.scene.getNonDeletedElementsMap(), + app.getEditorUIOffsets(), + ) + ) { + app.scrollToContent(matchAsElement, { + fitToContent: true, + animate: true, + duration: 300, + }); + } + + const nextMatches = searchMatches.items.map((match, index) => { + if (index === focusIndex) { + return { + id: match.textElement.id, + focus: true, + matchedLines: match.matchedLines, + }; + } + return { + id: match.textElement.id, + focus: false, + matchedLines: match.matchedLines, + }; + }); + + setAppState({ + searchMatches: nextMatches, + }); + } + } + }, [app, focusIndex, searchMatches, setAppState]); + + useEffect(() => { + return () => { + setFocusIndex(null); + searchedKeywordRef.current = null; + lastSceneNonceRef.current = undefined; + setAppState({ + searchMatches: [], + }); + }; + }, [setAppState, setFocusIndex]); + + const stableState = useStable({ + goToNextItem, + goToPreviousItem, + searchMatches, + }); + + useEffect(() => { + const eventHandler = (event: KeyboardEvent) => { + if ( + event.key === KEYS.ESCAPE && + !app.state.openDialog && + !app.state.openPopup + ) { + event.preventDefault(); + event.stopPropagation(); + setAppState({ + openSidebar: null, + }); + return; + } + + if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F) { + event.preventDefault(); + event.stopPropagation(); + + if (!searchInputRef.current?.matches(":focus")) { + if (app.state.openDialog) { + setAppState({ + openDialog: null, + }); + } + searchInputRef.current?.focus(); + } else { + setAppState({ + openSidebar: null, + }); + } + } + + if ( + event.target instanceof HTMLElement && + event.target.closest(".layer-ui__search") + ) { + if (stableState.searchMatches.items.length) { + if (event.key === KEYS.ENTER) { + event.stopPropagation(); + stableState.goToNextItem(); + } + + if (event.key === KEYS.ARROW_UP) { + event.stopPropagation(); + stableState.goToPreviousItem(); + } else if (event.key === KEYS.ARROW_DOWN) { + event.stopPropagation(); + stableState.goToNextItem(); + } + } + } + }; + + // `capture` needed to prevent firing on initial open from App.tsx, + // as well as to handle events before App ones + return addEventListener(window, EVENT.KEYDOWN, eventHandler, { + capture: true, + }); + }, [setAppState, stableState, app]); + + /** + * NOTE: + * + * for testing purposes, we're manually focusing instead of + * setting `selectOnRender` on + */ + useEffect(() => { + searchInputRef.current?.focus(); + }, []); + + const matchCount = `${searchMatches.items.length} ${ + searchMatches.items.length === 1 + ? t("search.singleResult") + : t("search.multipleResults") + }`; + + return ( +
+
+ { + setKeyword(value); + }} + /> +
+ +
+ {searchMatches.items.length > 0 && ( + <> + {focusIndex !== null && focusIndex > -1 ? ( +
+ {focusIndex + 1} / {matchCount} +
+ ) : ( +
{matchCount}
+ )} +
+ + +
+ + )} + + {searchMatches.items.length === 0 && + keyword && + searchedKeywordRef.current && ( +
{t("search.noMatch")}
+ )} +
+ + +
+ ); +}; + +const ListItem = (props: { + preview: SearchMatchItem["preview"]; + trimmedKeyword: string; + highlighted: boolean; + onClick?: () => void; +}) => { + const preview = [ + props.preview.moreBefore ? "..." : "", + props.preview.previewText.slice(0, props.preview.indexInKeyword), + props.preview.previewText.slice( + props.preview.indexInKeyword, + props.preview.indexInKeyword + props.trimmedKeyword.length, + ), + props.preview.previewText.slice( + props.preview.indexInKeyword + props.trimmedKeyword.length, + ), + props.preview.moreAfter ? "..." : "", + ]; + + return ( +
{ + if (props.highlighted) { + ref?.scrollIntoView({ behavior: "auto", block: "nearest" }); + } + }} + > +
+ {preview.flatMap((text, idx) => ( + {idx === 2 ? {text} : text} + ))} +
+
+ ); +}; + +interface MatchListProps { + matches: SearchMatches; + onItemClick: (index: number) => void; + focusIndex: number | null; + trimmedKeyword: string; +} + +const MatchListBase = (props: MatchListProps) => { + return ( +
+ {props.matches.items.map((searchMatch, index) => ( + props.onItemClick(index)} + /> + ))} +
+ ); +}; + +const areEqual = (prevProps: MatchListProps, nextProps: MatchListProps) => { + return ( + prevProps.matches.nonce === nextProps.matches.nonce && + prevProps.focusIndex === nextProps.focusIndex + ); +}; + +const MatchList = memo(MatchListBase, areEqual); + +const getMatchPreview = (text: string, index: number, keyword: string) => { + const WORDS_BEFORE = 2; + const WORDS_AFTER = 5; + + const substrBeforeKeyword = text.slice(0, index); + const wordsBeforeKeyword = substrBeforeKeyword.split(/\s+/); + // text = "small", keyword = "mall", not complete before + // text = "small", keyword = "smal", complete before + const isKeywordCompleteBefore = substrBeforeKeyword.endsWith(" "); + const startWordIndex = + wordsBeforeKeyword.length - + WORDS_BEFORE - + 1 - + (isKeywordCompleteBefore ? 0 : 1); + let wordsBeforeAsString = + wordsBeforeKeyword + .slice(startWordIndex <= 0 ? 0 : startWordIndex) + .join(" ") + (isKeywordCompleteBefore ? " " : ""); + + const MAX_ALLOWED_CHARS = 20; + + wordsBeforeAsString = + wordsBeforeAsString.length > MAX_ALLOWED_CHARS + ? wordsBeforeAsString.slice(-MAX_ALLOWED_CHARS) + : wordsBeforeAsString; + + const substrAfterKeyword = text.slice(index + keyword.length); + const wordsAfter = substrAfterKeyword.split(/\s+/); + // text = "small", keyword = "mall", complete after + // text = "small", keyword = "smal", not complete after + const isKeywordCompleteAfter = !substrAfterKeyword.startsWith(" "); + const numberOfWordsToTake = isKeywordCompleteAfter + ? WORDS_AFTER + 1 + : WORDS_AFTER; + const wordsAfterAsString = + (isKeywordCompleteAfter ? "" : " ") + + wordsAfter.slice(0, numberOfWordsToTake).join(" "); + + return { + indexInKeyword: wordsBeforeAsString.length, + previewText: wordsBeforeAsString + keyword + wordsAfterAsString, + moreBefore: startWordIndex > 0, + moreAfter: wordsAfter.length > numberOfWordsToTake, + }; +}; + +const normalizeWrappedText = ( + wrappedText: string, + originalText: string, +): string => { + const wrappedLines = wrappedText.split("\n"); + const normalizedLines: string[] = []; + let originalIndex = 0; + + for (let i = 0; i < wrappedLines.length; i++) { + let currentLine = wrappedLines[i]; + const nextLine = wrappedLines[i + 1]; + + if (nextLine) { + const nextLineIndexInOriginal = originalText.indexOf( + nextLine, + originalIndex, + ); + + if (nextLineIndexInOriginal > currentLine.length + originalIndex) { + let j = nextLineIndexInOriginal - (currentLine.length + originalIndex); + + while (j > 0) { + currentLine += " "; + j--; + } + } + } + + normalizedLines.push(currentLine); + originalIndex = originalIndex + currentLine.length; + } + + return normalizedLines.join("\n"); +}; + +const getMatchedLines = ( + textElement: ExcalidrawTextElement, + keyword: string, + index: number, +) => { + const normalizedText = normalizeWrappedText( + textElement.text, + textElement.originalText, + ); + + const lines = normalizedText.split("\n"); + + const lineIndexRanges = []; + let currentIndex = 0; + let lineNumber = 0; + + for (const line of lines) { + const startIndex = currentIndex; + const endIndex = startIndex + line.length - 1; + + lineIndexRanges.push({ + line, + startIndex, + endIndex, + lineNumber, + }); + + // Move to the next line's start index + currentIndex = endIndex + 1; + lineNumber++; + } + + let startIndex = index; + let remainingKeyword = textElement.originalText.slice( + index, + index + keyword.length, + ); + const matchedLines: { + offsetX: number; + offsetY: number; + width: number; + height: number; + }[] = []; + + for (const lineIndexRange of lineIndexRanges) { + if (remainingKeyword === "") { + break; + } + + if ( + startIndex >= lineIndexRange.startIndex && + startIndex <= lineIndexRange.endIndex + ) { + const matchCapacity = lineIndexRange.endIndex + 1 - startIndex; + const textToStart = lineIndexRange.line.slice( + 0, + startIndex - lineIndexRange.startIndex, + ); + + const matchedWord = remainingKeyword.slice(0, matchCapacity); + remainingKeyword = remainingKeyword.slice(matchCapacity); + + const offset = measureText( + textToStart, + getFontString(textElement), + textElement.lineHeight, + true, + ); + + // measureText returns a non-zero width for the empty string + // which is not what we're after here, hence the check and the correction + if (textToStart === "") { + offset.width = 0; + } + + if (textElement.textAlign !== "left" && lineIndexRange.line.length > 0) { + const lineLength = measureText( + lineIndexRange.line, + getFontString(textElement), + textElement.lineHeight, + true, + ); + + const spaceToStart = + textElement.textAlign === "center" + ? (textElement.width - lineLength.width) / 2 + : textElement.width - lineLength.width; + offset.width += spaceToStart; + } + + const { width, height } = measureText( + matchedWord, + getFontString(textElement), + textElement.lineHeight, + ); + + const offsetX = offset.width; + const offsetY = lineIndexRange.lineNumber * offset.height; + + matchedLines.push({ + offsetX, + offsetY, + width, + height, + }); + + startIndex += matchCapacity; + } + } + + return matchedLines; +}; + +const escapeSpecialCharacters = (string: string) => { + return string.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&"); +}; + +const handleSearch = debounce( + ( + keyword: string, + app: AppClassProperties, + cb: (matchItems: SearchMatchItem[], focusIndex: number | null) => void, + ) => { + if (!keyword || keyword === "") { + cb([], null); + return; + } + + const elements = app.scene.getNonDeletedElements(); + const texts = elements.filter((el) => + isTextElement(el), + ) as ExcalidrawTextElement[]; + + texts.sort((a, b) => a.y - b.y); + + const matchItems: SearchMatchItem[] = []; + + const regex = new RegExp(escapeSpecialCharacters(keyword), "gi"); + + for (const textEl of texts) { + let match = null; + const text = textEl.originalText; + + while ((match = regex.exec(text)) !== null) { + const preview = getMatchPreview(text, match.index, keyword); + const matchedLines = getMatchedLines(textEl, keyword, match.index); + + if (matchedLines.length > 0) { + matchItems.push({ + textElement: textEl, + keyword, + preview, + index: match.index, + matchedLines, + }); + } + } + } + + const visibleIds = new Set( + app.visibleElements.map((visibleElement) => visibleElement.id), + ); + + const focusIndex = + matchItems.findIndex((matchItem) => + visibleIds.has(matchItem.textElement.id), + ) ?? null; + + cb(matchItems, focusIndex); + }, + SEARCH_DEBOUNCE, +); diff --git a/packages/excalidraw/components/SearchSidebar.tsx b/packages/excalidraw/components/SearchSidebar.tsx new file mode 100644 index 0000000000..7cb93ac5f0 --- /dev/null +++ b/packages/excalidraw/components/SearchSidebar.tsx @@ -0,0 +1,29 @@ +import { SEARCH_SIDEBAR } from "../constants"; +import { t } from "../i18n"; +import { SearchMenu } from "./SearchMenu"; +import { Sidebar } from "./Sidebar/Sidebar"; + +export const SearchSidebar = () => { + return ( + + + +
+ {t("search.title")} +
+
+ +
+
+ ); +}; diff --git a/packages/excalidraw/components/TextField.scss b/packages/excalidraw/components/TextField.scss index 952c975928..c46cd2fe8c 100644 --- a/packages/excalidraw/components/TextField.scss +++ b/packages/excalidraw/components/TextField.scss @@ -3,16 +3,29 @@ .excalidraw { --ExcTextField--color: var(--color-on-surface); --ExcTextField--label-color: var(--color-on-surface); - --ExcTextField--background: transparent; + --ExcTextField--background: var(--color-surface-low); --ExcTextField--readonly--background: var(--color-surface-high); --ExcTextField--readonly--color: var(--color-on-surface); - --ExcTextField--border: var(--color-border-outline); + --ExcTextField--border: var(--color-gray-20); --ExcTextField--readonly--border: var(--color-border-outline-variant); --ExcTextField--border-hover: var(--color-brand-hover); --ExcTextField--border-active: var(--color-brand-active); --ExcTextField--placeholder: var(--color-border-outline-variant); .ExcTextField { + position: relative; + + svg { + position: absolute; + top: 50%; // 50% is not exactly in the center of the input + transform: translateY(-50%); + left: 0.75rem; + width: 1.25rem; + height: 1.25rem; + color: var(--color-gray-40); + z-index: 1; + } + &--fullWidth { width: 100%; flex-grow: 1; @@ -37,7 +50,6 @@ display: flex; flex-direction: row; align-items: center; - padding: 0 1rem; height: 3rem; @@ -45,6 +57,8 @@ border: 1px solid var(--ExcTextField--border); border-radius: 0.5rem; + padding: 0 0.75rem; + &:not(&--readonly) { &:hover { border-color: var(--ExcTextField--border-hover); @@ -80,10 +94,6 @@ width: 100%; - &::placeholder { - color: var(--ExcTextField--placeholder); - } - &:not(:focus) { &:hover { background-color: initial; @@ -105,5 +115,9 @@ } } } + + &--hasIcon .ExcTextField__input { + padding-left: 2.5rem; + } } } diff --git a/packages/excalidraw/components/TextField.tsx b/packages/excalidraw/components/TextField.tsx index 463ea2c2d4..e1d3461112 100644 --- a/packages/excalidraw/components/TextField.tsx +++ b/packages/excalidraw/components/TextField.tsx @@ -21,7 +21,9 @@ type TextFieldProps = { fullWidth?: boolean; selectOnRender?: boolean; + icon?: React.ReactNode; label?: string; + className?: string; placeholder?: string; isRedacted?: boolean; } & ({ value: string } | { defaultValue: string }); @@ -37,6 +39,8 @@ export const TextField = forwardRef( selectOnRender, onKeyDown, isRedacted = false, + icon, + className, ...rest }, ref, @@ -56,14 +60,16 @@ export const TextField = forwardRef( return (
{ innerRef.current?.focus(); }} > -
{label}
+ {icon} + {label &&
{label}
}
, tablerIconProps, ); + +export const upIcon = createIcon( + + + + , + tablerIconProps, +); diff --git a/packages/excalidraw/components/main-menu/DefaultItems.tsx b/packages/excalidraw/components/main-menu/DefaultItems.tsx index 26ef260006..bb3059db54 100644 --- a/packages/excalidraw/components/main-menu/DefaultItems.tsx +++ b/packages/excalidraw/components/main-menu/DefaultItems.tsx @@ -15,6 +15,7 @@ import { LoadIcon, MoonIcon, save, + searchIcon, SunIcon, TrashIcon, usersIcon, @@ -27,6 +28,7 @@ import { actionLoadScene, actionSaveToActiveFile, actionShortcuts, + actionToggleSearchMenu, actionToggleTheme, } from "../../actions"; import clsx from "clsx"; @@ -40,7 +42,6 @@ import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemConten import { THEME } from "../../constants"; import type { Theme } from "../../element/types"; import { trackEvent } from "../../analytics"; - import "./DefaultItems.scss"; export const LoadScene = () => { @@ -145,6 +146,27 @@ export const CommandPalette = (opts?: { className?: string }) => { }; CommandPalette.displayName = "CommandPalette"; +export const SearchMenu = (opts?: { className?: string }) => { + const { t } = useI18n(); + const actionManager = useExcalidrawActionManager(); + + return ( + { + actionManager.executeAction(actionToggleSearchMenu); + }} + shortcut={getShortcutFromShortcutName("searchMenu")} + aria-label={t("search.title")} + className={opts?.className} + > + {t("search.title")} + + ); +}; +SearchMenu.displayName = "SearchMenu"; + export const Help = () => { const { t } = useI18n(); diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index 9601807f6f..31982d4fbb 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -113,6 +113,7 @@ export const ENV = { export const CLASSES = { SHAPE_ACTIONS_MENU: "App-menu__left", ZOOM_ACTIONS: "zoom-actions", + SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper", }; /** @@ -382,6 +383,10 @@ export const DEFAULT_SIDEBAR = { defaultTab: LIBRARY_SIDEBAR_TAB, } as const; +export const SEARCH_SIDEBAR = { + name: "search", +}; + export const LIBRARY_DISABLED_TYPES = new Set([ "iframe", "embeddable", diff --git a/packages/excalidraw/css/theme.scss b/packages/excalidraw/css/theme.scss index 0ed6a75449..69c28afdab 100644 --- a/packages/excalidraw/css/theme.scss +++ b/packages/excalidraw/css/theme.scss @@ -144,9 +144,9 @@ --border-radius-md: 0.375rem; --border-radius-lg: 0.5rem; - --color-surface-high: hsl(244, 100%, 97%); - --color-surface-mid: hsl(240 25% 96%); - --color-surface-low: hsl(240 25% 94%); + --color-surface-high: #f1f0ff; + --color-surface-mid: #f2f2f7; + --color-surface-low: #ececf4; --color-surface-lowest: #ffffff; --color-on-surface: #1b1b1f; --color-brand-hover: #5753d0; diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index 9fb4766b6e..9abebc3563 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -284,16 +284,17 @@ export const measureText = ( text: string, font: FontString, lineHeight: ExcalidrawTextElement["lineHeight"], + forceAdvanceWidth?: true, ) => { - text = text + const _text = text .split("\n") // replace empty lines with single space because leading/trailing empty // lines would be stripped from computation .map((x) => x || " ") .join("\n"); const fontSize = parseFloat(font); - const height = getTextHeight(text, fontSize, lineHeight); - const width = getTextWidth(text, font); + const height = getTextHeight(_text, fontSize, lineHeight); + const width = getTextWidth(_text, font, forceAdvanceWidth); return { width, height }; }; diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index ebf9ff8725..ff1fa20263 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -162,6 +162,13 @@ "hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.", "hint_emptyPrivateLibrary": "Select an item on canvas to add it here." }, + "search": { + "title": "Find on canvas", + "noMatch": "No matches found...", + "singleResult": "result", + "multipleResults": "results", + "placeholder": "Find text..." + }, "buttons": { "clearReset": "Reset the canvas", "exportJSON": "Export to file", @@ -297,6 +304,7 @@ "shapes": "Shapes" }, "hints": { + "dismissSearch": "Escape to dismiss search", "canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool", "linearElement": "Click to start multiple points, drag for single line", "arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.", diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 5a27a3312d..0d03b0f5a0 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -30,8 +30,12 @@ import { shouldShowBoundingBox, } from "../element/transformHandles"; import { arrayToMap, throttleRAF } from "../utils"; -import type { InteractiveCanvasAppState } from "../types"; -import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants"; +import { + DEFAULT_TRANSFORM_HANDLE_SPACING, + FRAME_STYLE, + THEME, +} from "../constants"; +import { type InteractiveCanvasAppState } from "../types"; import { renderSnaps } from "../renderer/renderSnaps"; @@ -952,9 +956,48 @@ const _renderInteractiveScene = ({ context.restore(); } + appState.searchMatches.forEach(({ id, focus, matchedLines }) => { + const element = elementsMap.get(id); + + if (element && isTextElement(element)) { + const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + true, + ); + + context.save(); + if (appState.theme === THEME.LIGHT) { + if (focus) { + context.fillStyle = "rgba(255, 124, 0, 0.4)"; + } else { + context.fillStyle = "rgba(255, 226, 0, 0.4)"; + } + } else if (focus) { + context.fillStyle = "rgba(229, 82, 0, 0.4)"; + } else { + context.fillStyle = "rgba(99, 52, 0, 0.4)"; + } + + context.translate(appState.scrollX, appState.scrollY); + context.translate(cx, cy); + context.rotate(element.angle); + + matchedLines.forEach((matchedLine) => { + context.fillRect( + elementX1 + matchedLine.offsetX - cx, + elementY1 + matchedLine.offsetY - cy, + matchedLine.width, + matchedLine.height, + ); + }); + + context.restore(); + } + }); + renderSnaps(context, appState); - // Reset zoom context.restore(); renderRemoteCursors({ diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 7f9904a4d8..3a5e140658 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -866,6 +866,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -1068,6 +1069,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1283,6 +1285,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1613,6 +1616,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1943,6 +1947,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -2158,6 +2163,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2397,6 +2403,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0_copy": true, }, @@ -2699,6 +2706,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -3065,6 +3073,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -3539,6 +3548,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, @@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, @@ -4185,6 +4196,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -5370,6 +5382,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -6496,6 +6509,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -7431,6 +7445,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8339,6 +8354,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -9235,6 +9251,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap index 2994cfc3eb..e5e431dfc6 100644 --- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap @@ -239,6 +239,55 @@ exports[` > Test UIOptions prop > Test canvasActions > should rende Ctrl+Shift+E
+
diff --git a/packages/excalidraw/components/TextField.tsx b/packages/excalidraw/components/TextField.tsx index e1d3461112..c5bdc82609 100644 --- a/packages/excalidraw/components/TextField.tsx +++ b/packages/excalidraw/components/TextField.tsx @@ -51,6 +51,8 @@ export const TextField = forwardRef( useLayoutEffect(() => { if (selectOnRender) { + // focusing first is needed because vitest/jsdom + innerRef.current?.focus(); innerRef.current?.select(); } }, [selectOnRender]); diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 409d5825b7..6c16e51909 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -76,11 +76,12 @@ export class API { }); }; - static updateElement = ( - ...[element, updates]: Parameters + // eslint-disable-next-line prettier/prettier + static updateElement = ( + ...args: Parameters> ) => { act(() => { - mutateElement(element, updates); + mutateElement(...args); }); }; diff --git a/packages/excalidraw/tests/search.test.tsx b/packages/excalidraw/tests/search.test.tsx index bed596c438..ae729b2101 100644 --- a/packages/excalidraw/tests/search.test.tsx +++ b/packages/excalidraw/tests/search.test.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { render, waitFor } from "./test-utils"; -import { Excalidraw, mutateElement } from "../index"; +import { act, render, waitFor } from "./test-utils"; +import { Excalidraw } from "../index"; import { CLASSES, SEARCH_SIDEBAR } from "../constants"; import { Keyboard } from "./helpers/ui"; import { KEYS } from "../keys"; @@ -22,7 +22,7 @@ const querySearchInput = async () => { describe("search", () => { beforeEach(async () => { await render(); - h.setState({ + API.setAppState({ openSidebar: null, }); }); @@ -50,7 +50,9 @@ describe("search", () => { `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, ); - searchInput?.blur(); + act(() => { + searchInput?.blur(); + }); expect(h.app.state.openSidebar).not.toBeNull(); expect(searchInput?.matches(":focus")).toBe(false); @@ -109,7 +111,7 @@ describe("search", () => { }), ]); - mutateElement(h.elements[0] as ExcalidrawTextElement, { + API.updateElement(h.elements[0] as ExcalidrawTextElement, { text: "t\ne\ns\nt \nt\ne\nx\nt \ns\np\nli\nt \ni\nn\nt\no\nm\nu\nlt\ni\np\nl\ne \nli\nn\ne\ns", originalText: "test text split into multiple lines", }); From 72b7c937b1b9cd4d1155563f15ccbbac5c71f8dc Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:26:01 +0200 Subject: [PATCH 05/26] feat: smarter zooming when scrolling to match & only match on search/switch (#8488) --- excalidraw-app/data/LocalData.ts | 9 +- packages/excalidraw/components/SearchMenu.tsx | 200 +++++++++++------- 2 files changed, 127 insertions(+), 82 deletions(-) diff --git a/excalidraw-app/data/LocalData.ts b/excalidraw-app/data/LocalData.ts index 468126b2bb..df753c89b6 100644 --- a/excalidraw-app/data/LocalData.ts +++ b/excalidraw-app/data/LocalData.ts @@ -20,6 +20,7 @@ import { get, } from "idb-keyval"; import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; +import { SEARCH_SIDEBAR } from "../../packages/excalidraw/constants"; import type { LibraryPersistedData } from "../../packages/excalidraw/data/library"; import type { ImportedDataState } from "../../packages/excalidraw/data/types"; import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; @@ -66,13 +67,19 @@ const saveDataStateToLocalStorage = ( appState: AppState, ) => { try { + const _appState = clearAppStateForLocalStorage(appState); + + if (_appState.openSidebar?.name === SEARCH_SIDEBAR.name) { + _appState.openSidebar = null; + } + localStorage.setItem( STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS, JSON.stringify(clearElementsForLocalStorage(elements)), ); localStorage.setItem( STORAGE_KEYS.LOCAL_STORAGE_APP_STATE, - JSON.stringify(clearAppStateForLocalStorage(appState)), + JSON.stringify(_appState), ); updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE); } catch (error: any) { diff --git a/packages/excalidraw/components/SearchMenu.tsx b/packages/excalidraw/components/SearchMenu.tsx index a7bf9b4473..58dd622dd7 100644 --- a/packages/excalidraw/components/SearchMenu.tsx +++ b/packages/excalidraw/components/SearchMenu.tsx @@ -21,17 +21,17 @@ import { useStable } from "../hooks/useStable"; import "./SearchMenu.scss"; -const searchKeywordAtom = atom(""); +const searchQueryAtom = atom(""); export const searchItemInFocusAtom = atom(null); const SEARCH_DEBOUNCE = 350; type SearchMatchItem = { textElement: ExcalidrawTextElement; - keyword: string; + searchQuery: SearchQuery; index: number; preview: { - indexInKeyword: number; + indexInSearchQuery: number; previewText: string; moreBefore: boolean; moreAfter: boolean; @@ -49,19 +49,25 @@ type SearchMatches = { items: SearchMatchItem[]; }; +type SearchQuery = string & { _brand: "SearchQuery" }; + export const SearchMenu = () => { const app = useApp(); const setAppState = useExcalidrawSetAppState(); const searchInputRef = useRef(null); - const [keyword, setKeyword] = useAtom(searchKeywordAtom, jotaiScope); + const [inputValue, setInputValue] = useAtom(searchQueryAtom, jotaiScope); + const searchQuery = inputValue.trim() as SearchQuery; + + const [isSearching, setIsSearching] = useState(false); + const [searchMatches, setSearchMatches] = useState({ nonce: null, items: [], }); - const searchedKeywordRef = useRef(); - const lastSceneNonceRef = useRef(); + const searchedQueryRef = useRef(null); + const lastSceneNonceRef = useRef(undefined); const [focusIndex, setFocusIndex] = useAtom( searchItemInFocusAtom, @@ -70,19 +76,20 @@ export const SearchMenu = () => { const elementsMap = app.scene.getNonDeletedElementsMap(); useEffect(() => { - const trimmedKeyword = keyword.trim(); + if (isSearching) { + return; + } if ( - trimmedKeyword !== searchedKeywordRef.current || + searchQuery !== searchedQueryRef.current || app.scene.getSceneNonce() !== lastSceneNonceRef.current ) { - searchedKeywordRef.current = null; - handleSearch(trimmedKeyword, app, (matchItems, index) => { + searchedQueryRef.current = null; + handleSearch(searchQuery, app, (matchItems, index) => { setSearchMatches({ nonce: randomInteger(), items: matchItems, }); - setFocusIndex(index); - searchedKeywordRef.current = trimmedKeyword; + searchedQueryRef.current = searchQuery; lastSceneNonceRef.current = app.scene.getSceneNonce(); setAppState({ searchMatches: matchItems.map((searchMatch) => ({ @@ -94,7 +101,8 @@ export const SearchMenu = () => { }); } }, [ - keyword, + isSearching, + searchQuery, elementsMap, app, setAppState, @@ -128,19 +136,35 @@ export const SearchMenu = () => { } }; + useEffect(() => { + setAppState((state) => { + return { + searchMatches: state.searchMatches.map((match, index) => { + if (index === focusIndex) { + return { ...match, focus: true }; + } + return { ...match, focus: false }; + }), + }; + }); + }, [focusIndex, setAppState]); + useEffect(() => { if (searchMatches.items.length > 0 && focusIndex !== null) { const match = searchMatches.items[focusIndex]; if (match) { const matchAsElement = newTextElement({ - text: match.keyword, + text: match.searchQuery, x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0), y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0), width: match.matchedLines[0]?.width, height: match.matchedLines[0]?.height, }); + const isTextTiny = + match.textElement.fontSize * app.state.zoom.value < 12; + if ( !isElementCompletelyInViewport( [matchAsElement], @@ -155,45 +179,36 @@ export const SearchMenu = () => { }, app.scene.getNonDeletedElementsMap(), app.getEditorUIOffsets(), - ) + ) || + isTextTiny ) { + let zoomOptions: Parameters[1]; + + if (isTextTiny && app.state.zoom.value >= 1) { + zoomOptions = { fitToViewport: true }; + } else if (isTextTiny || app.state.zoom.value > 1) { + zoomOptions = { fitToContent: true }; + } + app.scrollToContent(matchAsElement, { - fitToContent: true, animate: true, duration: 300, + ...zoomOptions, }); } - - const nextMatches = searchMatches.items.map((match, index) => { - if (index === focusIndex) { - return { - id: match.textElement.id, - focus: true, - matchedLines: match.matchedLines, - }; - } - return { - id: match.textElement.id, - focus: false, - matchedLines: match.matchedLines, - }; - }); - - setAppState({ - searchMatches: nextMatches, - }); } } - }, [app, focusIndex, searchMatches, setAppState]); + }, [focusIndex, searchMatches, app]); useEffect(() => { return () => { setFocusIndex(null); - searchedKeywordRef.current = null; + searchedQueryRef.current = null; lastSceneNonceRef.current = undefined; setAppState({ searchMatches: [], }); + setIsSearching(false); }; }, [setAppState, setFocusIndex]); @@ -276,12 +291,32 @@ export const SearchMenu = () => {
{ - setKeyword(value); + setInputValue(value); + setIsSearching(true); + const searchQuery = value.trim() as SearchQuery; + handleSearch(searchQuery, app, (matchItems, index) => { + setSearchMatches({ + nonce: randomInteger(), + items: matchItems, + }); + setFocusIndex(index); + searchedQueryRef.current = searchQuery; + lastSceneNonceRef.current = app.scene.getSceneNonce(); + setAppState({ + searchMatches: matchItems.map((searchMatch) => ({ + id: searchMatch.textElement.id, + focus: false, + matchedLines: searchMatch.matchedLines, + })), + }); + + setIsSearching(false); + }); }} selectOnRender /> @@ -319,8 +354,8 @@ export const SearchMenu = () => { )} {searchMatches.items.length === 0 && - keyword && - searchedKeywordRef.current && ( + searchQuery && + searchedQueryRef.current && (
{t("search.noMatch")}
)}
@@ -329,7 +364,7 @@ export const SearchMenu = () => { matches={searchMatches} onItemClick={setFocusIndex} focusIndex={focusIndex} - trimmedKeyword={keyword.trim()} + searchQuery={searchQuery} /> ); @@ -337,19 +372,19 @@ export const SearchMenu = () => { const ListItem = (props: { preview: SearchMatchItem["preview"]; - trimmedKeyword: string; + searchQuery: SearchQuery; highlighted: boolean; onClick?: () => void; }) => { const preview = [ props.preview.moreBefore ? "..." : "", - props.preview.previewText.slice(0, props.preview.indexInKeyword), + props.preview.previewText.slice(0, props.preview.indexInSearchQuery), props.preview.previewText.slice( - props.preview.indexInKeyword, - props.preview.indexInKeyword + props.trimmedKeyword.length, + props.preview.indexInSearchQuery, + props.preview.indexInSearchQuery + props.searchQuery.length, ), props.preview.previewText.slice( - props.preview.indexInKeyword + props.trimmedKeyword.length, + props.preview.indexInSearchQuery + props.searchQuery.length, ), props.preview.moreAfter ? "..." : "", ]; @@ -380,7 +415,7 @@ interface MatchListProps { matches: SearchMatches; onItemClick: (index: number) => void; focusIndex: number | null; - trimmedKeyword: string; + searchQuery: SearchQuery; } const MatchListBase = (props: MatchListProps) => { @@ -389,7 +424,7 @@ const MatchListBase = (props: MatchListProps) => { {props.matches.items.map((searchMatch, index) => ( props.onItemClick(index)} @@ -408,24 +443,27 @@ const areEqual = (prevProps: MatchListProps, nextProps: MatchListProps) => { const MatchList = memo(MatchListBase, areEqual); -const getMatchPreview = (text: string, index: number, keyword: string) => { +const getMatchPreview = ( + text: string, + index: number, + searchQuery: SearchQuery, +) => { const WORDS_BEFORE = 2; const WORDS_AFTER = 5; - const substrBeforeKeyword = text.slice(0, index); - const wordsBeforeKeyword = substrBeforeKeyword.split(/\s+/); - // text = "small", keyword = "mall", not complete before - // text = "small", keyword = "smal", complete before - const isKeywordCompleteBefore = substrBeforeKeyword.endsWith(" "); + const substrBeforeQuery = text.slice(0, index); + const wordsBeforeQuery = substrBeforeQuery.split(/\s+/); + // text = "small", query = "mall", not complete before + // text = "small", query = "smal", complete before + const isQueryCompleteBefore = substrBeforeQuery.endsWith(" "); const startWordIndex = - wordsBeforeKeyword.length - + wordsBeforeQuery.length - WORDS_BEFORE - 1 - - (isKeywordCompleteBefore ? 0 : 1); + (isQueryCompleteBefore ? 0 : 1); let wordsBeforeAsString = - wordsBeforeKeyword - .slice(startWordIndex <= 0 ? 0 : startWordIndex) - .join(" ") + (isKeywordCompleteBefore ? " " : ""); + wordsBeforeQuery.slice(startWordIndex <= 0 ? 0 : startWordIndex).join(" ") + + (isQueryCompleteBefore ? " " : ""); const MAX_ALLOWED_CHARS = 20; @@ -434,21 +472,21 @@ const getMatchPreview = (text: string, index: number, keyword: string) => { ? wordsBeforeAsString.slice(-MAX_ALLOWED_CHARS) : wordsBeforeAsString; - const substrAfterKeyword = text.slice(index + keyword.length); - const wordsAfter = substrAfterKeyword.split(/\s+/); - // text = "small", keyword = "mall", complete after - // text = "small", keyword = "smal", not complete after - const isKeywordCompleteAfter = !substrAfterKeyword.startsWith(" "); - const numberOfWordsToTake = isKeywordCompleteAfter + const substrAfterQuery = text.slice(index + searchQuery.length); + const wordsAfter = substrAfterQuery.split(/\s+/); + // text = "small", query = "mall", complete after + // text = "small", query = "smal", not complete after + const isQueryCompleteAfter = !substrAfterQuery.startsWith(" "); + const numberOfWordsToTake = isQueryCompleteAfter ? WORDS_AFTER + 1 : WORDS_AFTER; const wordsAfterAsString = - (isKeywordCompleteAfter ? "" : " ") + + (isQueryCompleteAfter ? "" : " ") + wordsAfter.slice(0, numberOfWordsToTake).join(" "); return { - indexInKeyword: wordsBeforeAsString.length, - previewText: wordsBeforeAsString + keyword + wordsAfterAsString, + indexInSearchQuery: wordsBeforeAsString.length, + previewText: wordsBeforeAsString + searchQuery + wordsAfterAsString, moreBefore: startWordIndex > 0, moreAfter: wordsAfter.length > numberOfWordsToTake, }; @@ -491,7 +529,7 @@ const normalizeWrappedText = ( const getMatchedLines = ( textElement: ExcalidrawTextElement, - keyword: string, + searchQuery: SearchQuery, index: number, ) => { const normalizedText = normalizeWrappedText( @@ -522,9 +560,9 @@ const getMatchedLines = ( } let startIndex = index; - let remainingKeyword = textElement.originalText.slice( + let remainingQuery = textElement.originalText.slice( index, - index + keyword.length, + index + searchQuery.length, ); const matchedLines: { offsetX: number; @@ -534,7 +572,7 @@ const getMatchedLines = ( }[] = []; for (const lineIndexRange of lineIndexRanges) { - if (remainingKeyword === "") { + if (remainingQuery === "") { break; } @@ -548,8 +586,8 @@ const getMatchedLines = ( startIndex - lineIndexRange.startIndex, ); - const matchedWord = remainingKeyword.slice(0, matchCapacity); - remainingKeyword = remainingKeyword.slice(matchCapacity); + const matchedWord = remainingQuery.slice(0, matchCapacity); + remainingQuery = remainingQuery.slice(matchCapacity); const offset = measureText( textToStart, @@ -608,11 +646,11 @@ const escapeSpecialCharacters = (string: string) => { const handleSearch = debounce( ( - keyword: string, + searchQuery: SearchQuery, app: AppClassProperties, cb: (matchItems: SearchMatchItem[], focusIndex: number | null) => void, ) => { - if (!keyword || keyword === "") { + if (!searchQuery || searchQuery === "") { cb([], null); return; } @@ -626,20 +664,20 @@ const handleSearch = debounce( const matchItems: SearchMatchItem[] = []; - const regex = new RegExp(escapeSpecialCharacters(keyword), "gi"); + const regex = new RegExp(escapeSpecialCharacters(searchQuery), "gi"); for (const textEl of texts) { let match = null; const text = textEl.originalText; while ((match = regex.exec(text)) !== null) { - const preview = getMatchPreview(text, match.index, keyword); - const matchedLines = getMatchedLines(textEl, keyword, match.index); + const preview = getMatchPreview(text, match.index, searchQuery); + const matchedLines = getMatchedLines(textEl, searchQuery, match.index); if (matchedLines.length > 0) { matchItems.push({ textElement: textEl, - keyword, + searchQuery, preview, index: match.index, matchedLines, From b46ca0192b4df0c12bd672205529ba6c6b16954b Mon Sep 17 00:00:00 2001 From: zsviczian Date: Wed, 11 Sep 2024 07:57:41 +0200 Subject: [PATCH 06/26] fix: addFiles clears the whole image cache when each file is added - regression from #8471 (#8490) Update App.tsx --- packages/excalidraw/components/App.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index e067bba7d1..8276b88f4f 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -2367,9 +2367,10 @@ class App extends React.Component { return false; }; - private clearImageShapeCache() { + private clearImageShapeCache(filesMap?: BinaryFiles) { + const files = filesMap ?? this.files; this.scene.getNonDeletedElements().forEach((element) => { - if (isInitializedImageElement(element) && this.files[element.fileId]) { + if (isInitializedImageElement(element) && files[element.fileId]) { this.imageCache.delete(element.fileId); ShapeCache.delete(element); } @@ -3690,7 +3691,7 @@ class App extends React.Component { this.files = { ...this.files, ...Object.fromEntries(filesMap) }; - this.clearImageShapeCache(); + this.clearImageShapeCache(Object.fromEntries(filesMap)); this.scene.triggerUpdate(); this.addNewImagesToImageCache(); From fd39712ba6c566e325fccc751131235242d228c5 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:01:18 +0200 Subject: [PATCH 07/26] fix: improve canvas search scroll behavior further (#8491) --- packages/excalidraw/actions/actionCanvas.tsx | 107 ++++++++++-------- packages/excalidraw/components/App.tsx | 70 +++++++----- packages/excalidraw/components/SearchMenu.tsx | 25 +++- packages/excalidraw/element/newElement.ts | 1 - packages/excalidraw/element/sizeHelpers.ts | 9 +- packages/excalidraw/scene/scroll.ts | 20 +++- packages/excalidraw/types.ts | 7 ++ packages/math/utils.ts | 23 +++- 8 files changed, 164 insertions(+), 98 deletions(-) diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 35fabcaf91..83b0ad5290 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -24,7 +24,7 @@ import { CODES, KEYS } from "../keys"; import { getNormalizedZoom } from "../scene"; import { centerScrollOn } from "../scene/scroll"; import { getStateForZoom } from "../scene/zoom"; -import type { AppState } from "../types"; +import type { AppState, Offsets } from "../types"; import { getShortcutKey, updateActiveTool } from "../utils"; import { register } from "./register"; import { Tooltip } from "../components/Tooltip"; @@ -38,7 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import type { SceneBounds } from "../element/bounds"; import { setCursor } from "../cursor"; import { StoreAction } from "../store"; -import { clamp } from "../../math"; +import { clamp, roundToStep } from "../../math"; export const actionChangeViewBackgroundColor = register({ name: "changeViewBackgroundColor", @@ -259,89 +259,85 @@ const zoomValueToFitBoundsOnViewport = ( const adjustedZoomValue = smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1); - const zoomAdjustedToSteps = - Math.floor(adjustedZoomValue / ZOOM_STEP) * ZOOM_STEP; - - return getNormalizedZoom(Math.min(zoomAdjustedToSteps, 1)); + return Math.min(adjustedZoomValue, 1); }; export const zoomToFitBounds = ({ bounds, appState, + canvasOffsets, fitToViewport = false, viewportZoomFactor = 1, + minZoom = -Infinity, + maxZoom = Infinity, }: { bounds: SceneBounds; + canvasOffsets?: Offsets; appState: Readonly; /** whether to fit content to viewport (beyond >100%) */ fitToViewport: boolean; /** zoom content to cover X of the viewport, when fitToViewport=true */ viewportZoomFactor?: number; + minZoom?: number; + maxZoom?: number; }) => { + viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM); + const [x1, y1, x2, y2] = bounds; const centerX = (x1 + x2) / 2; const centerY = (y1 + y2) / 2; - let newZoomValue; - let scrollX; - let scrollY; + const canvasOffsetLeft = canvasOffsets?.left ?? 0; + const canvasOffsetTop = canvasOffsets?.top ?? 0; + const canvasOffsetRight = canvasOffsets?.right ?? 0; + const canvasOffsetBottom = canvasOffsets?.bottom ?? 0; + + const effectiveCanvasWidth = + appState.width - canvasOffsetLeft - canvasOffsetRight; + const effectiveCanvasHeight = + appState.height - canvasOffsetTop - canvasOffsetBottom; + + let adjustedZoomValue; if (fitToViewport) { const commonBoundsWidth = x2 - x1; const commonBoundsHeight = y2 - y1; - newZoomValue = + adjustedZoomValue = Math.min( - appState.width / commonBoundsWidth, - appState.height / commonBoundsHeight, - ) * clamp(viewportZoomFactor, 0.1, 1); - - newZoomValue = getNormalizedZoom(newZoomValue); - - let appStateWidth = appState.width; - - if (appState.openSidebar) { - const sidebarDOMElem = document.querySelector( - ".sidebar", - ) as HTMLElement | null; - const sidebarWidth = sidebarDOMElem?.offsetWidth ?? 0; - const isRTL = document.documentElement.getAttribute("dir") === "rtl"; - - appStateWidth = !isRTL - ? appState.width - sidebarWidth - : appState.width + sidebarWidth; - } - - scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX; - scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY; + effectiveCanvasWidth / commonBoundsWidth, + effectiveCanvasHeight / commonBoundsHeight, + ) * viewportZoomFactor; } else { - newZoomValue = zoomValueToFitBoundsOnViewport( + adjustedZoomValue = zoomValueToFitBoundsOnViewport( bounds, { - width: appState.width, - height: appState.height, + width: effectiveCanvasWidth, + height: effectiveCanvasHeight, }, viewportZoomFactor, ); + } - const centerScroll = centerScrollOn({ - scenePoint: { x: centerX, y: centerY }, - viewportDimensions: { - width: appState.width, - height: appState.height, - }, - zoom: { value: newZoomValue }, - }); + const newZoomValue = getNormalizedZoom( + clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom), + ); - scrollX = centerScroll.scrollX; - scrollY = centerScroll.scrollY; - } + const centerScroll = centerScrollOn({ + scenePoint: { x: centerX, y: centerY }, + viewportDimensions: { + width: appState.width, + height: appState.height, + }, + offsets: canvasOffsets, + zoom: { value: newZoomValue }, + }); return { appState: { ...appState, - scrollX, - scrollY, + scrollX: centerScroll.scrollX, + scrollY: centerScroll.scrollY, zoom: { value: newZoomValue }, }, storeAction: StoreAction.NONE, @@ -349,25 +345,34 @@ export const zoomToFitBounds = ({ }; export const zoomToFit = ({ + canvasOffsets, targetElements, appState, fitToViewport, viewportZoomFactor, + minZoom, + maxZoom, }: { + canvasOffsets?: Offsets; targetElements: readonly ExcalidrawElement[]; appState: Readonly; /** whether to fit content to viewport (beyond >100%) */ fitToViewport: boolean; /** zoom content to cover X of the viewport, when fitToViewport=true */ viewportZoomFactor?: number; + minZoom?: number; + maxZoom?: number; }) => { const commonBounds = getCommonBounds(getNonDeletedElements(targetElements)); return zoomToFitBounds({ + canvasOffsets, bounds: commonBounds, appState, fitToViewport, viewportZoomFactor, + minZoom, + maxZoom, }); }; @@ -388,6 +393,7 @@ export const actionZoomToFitSelectionInViewport = register({ userToFollow: null, }, fitToViewport: false, + canvasOffsets: app.getEditorUIOffsets(), }); }, // NOTE shift-2 should have been assigned actionZoomToFitSelection. @@ -413,7 +419,7 @@ export const actionZoomToFitSelection = register({ userToFollow: null, }, fitToViewport: true, - viewportZoomFactor: 0.7, + canvasOffsets: app.getEditorUIOffsets(), }); }, // NOTE this action should use shift-2 per figma, alas @@ -430,7 +436,7 @@ export const actionZoomToFit = register({ icon: zoomAreaIcon, viewMode: true, trackEvent: { category: "canvas" }, - perform: (elements, appState) => + perform: (elements, appState, _, app) => zoomToFit({ targetElements: elements, appState: { @@ -438,6 +444,7 @@ export const actionZoomToFit = register({ userToFollow: null, }, fitToViewport: false, + canvasOffsets: app.getEditorUIOffsets(), }), keyTest: (event) => event.code === CODES.ONE && diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 8276b88f4f..fb4ac27b12 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -259,6 +259,7 @@ import type { ElementsPendingErasure, GenerateDiagramToCode, NullableGridSize, + Offsets, } from "../types"; import { debounce, @@ -3232,6 +3233,7 @@ class App extends React.Component { if (opts.fitToContent) { this.scrollToContent(newElements, { fitToContent: true, + canvasOffsets: this.getEditorUIOffsets(), }); } }; @@ -3544,7 +3546,7 @@ class App extends React.Component { target: | ExcalidrawElement | readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(), - opts?: + opts?: ( | { fitToContent?: boolean; fitToViewport?: never; @@ -3561,7 +3563,12 @@ class App extends React.Component { viewportZoomFactor?: number; animate?: boolean; duration?: number; - }, + } + ) & { + minZoom?: number; + maxZoom?: number; + canvasOffsets?: Offsets; + }, ) => { this.cancelInProgressAnimation?.(); @@ -3574,10 +3581,13 @@ class App extends React.Component { if (opts?.fitToContent || opts?.fitToViewport) { const { appState } = zoomToFit({ + canvasOffsets: opts.canvasOffsets, targetElements, appState: this.state, fitToViewport: !!opts?.fitToViewport, viewportZoomFactor: opts?.viewportZoomFactor, + minZoom: opts?.minZoom, + maxZoom: opts?.maxZoom, }); zoom = appState.zoom; scrollX = appState.scrollX; @@ -3805,40 +3815,42 @@ class App extends React.Component { }, ); - public getEditorUIOffsets = (): { - top: number; - right: number; - bottom: number; - left: number; - } => { + public getEditorUIOffsets = (): Offsets => { const toolbarBottom = this.excalidrawContainerRef?.current ?.querySelector(".App-toolbar") ?.getBoundingClientRect()?.bottom ?? 0; - const sidebarWidth = Math.max( - this.excalidrawContainerRef?.current - ?.querySelector(".default-sidebar") - ?.getBoundingClientRect()?.width ?? 0, - ); - const propertiesPanelWidth = Math.max( - this.excalidrawContainerRef?.current - ?.querySelector(".App-menu__left") - ?.getBoundingClientRect()?.width ?? 0, - 0, - ); + const sidebarRect = this.excalidrawContainerRef?.current + ?.querySelector(".sidebar") + ?.getBoundingClientRect(); + const propertiesPanelRect = this.excalidrawContainerRef?.current + ?.querySelector(".App-menu__left") + ?.getBoundingClientRect(); + + const PADDING = 16; return getLanguage().rtl ? { - top: toolbarBottom, - right: propertiesPanelWidth, - bottom: 0, - left: sidebarWidth, + top: toolbarBottom + PADDING, + right: + Math.max( + this.state.width - + (propertiesPanelRect?.left ?? this.state.width), + 0, + ) + PADDING, + bottom: PADDING, + left: Math.max(sidebarRect?.right ?? 0, 0) + PADDING, } : { - top: toolbarBottom, - right: sidebarWidth, - bottom: 0, - left: propertiesPanelWidth, + top: toolbarBottom + PADDING, + right: Math.max( + this.state.width - + (sidebarRect?.left ?? this.state.width) + + PADDING, + 0, + ), + bottom: PADDING, + left: Math.max(propertiesPanelRect?.right ?? 0, 0) + PADDING, }; }; @@ -3923,7 +3935,7 @@ class App extends React.Component { animate: true, duration: 300, fitToContent: true, - viewportZoomFactor: 0.8, + canvasOffsets: this.getEditorUIOffsets(), }); } @@ -3979,6 +3991,7 @@ class App extends React.Component { this.scrollToContent(nextNode, { animate: true, duration: 300, + canvasOffsets: this.getEditorUIOffsets(), }); } } @@ -4411,6 +4424,7 @@ class App extends React.Component { this.scrollToContent(firstNode, { animate: true, duration: 300, + canvasOffsets: this.getEditorUIOffsets(), }); } } diff --git a/packages/excalidraw/components/SearchMenu.tsx b/packages/excalidraw/components/SearchMenu.tsx index 58dd622dd7..36922b0a5e 100644 --- a/packages/excalidraw/components/SearchMenu.tsx +++ b/packages/excalidraw/components/SearchMenu.tsx @@ -20,6 +20,7 @@ import { CLASSES, EVENT } from "../constants"; import { useStable } from "../hooks/useStable"; import "./SearchMenu.scss"; +import { round } from "../../math"; const searchQueryAtom = atom(""); export const searchItemInFocusAtom = atom(null); @@ -154,16 +155,23 @@ export const SearchMenu = () => { const match = searchMatches.items[focusIndex]; if (match) { + const zoomValue = app.state.zoom.value; + const matchAsElement = newTextElement({ text: match.searchQuery, x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0), y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0), width: match.matchedLines[0]?.width, height: match.matchedLines[0]?.height, + fontSize: match.textElement.fontSize, + fontFamily: match.textElement.fontFamily, }); + const FONT_SIZE_LEGIBILITY_THRESHOLD = 14; + + const fontSize = match.textElement.fontSize; const isTextTiny = - match.textElement.fontSize * app.state.zoom.value < 12; + fontSize * zoomValue < FONT_SIZE_LEGIBILITY_THRESHOLD; if ( !isElementCompletelyInViewport( @@ -184,9 +192,17 @@ export const SearchMenu = () => { ) { let zoomOptions: Parameters[1]; - if (isTextTiny && app.state.zoom.value >= 1) { - zoomOptions = { fitToViewport: true }; - } else if (isTextTiny || app.state.zoom.value > 1) { + if (isTextTiny) { + if (fontSize >= FONT_SIZE_LEGIBILITY_THRESHOLD) { + zoomOptions = { fitToContent: true }; + } else { + zoomOptions = { + fitToViewport: true, + // calculate zoom level to make the fontSize ~equal to FONT_SIZE_THRESHOLD, rounded to nearest 10% + maxZoom: round(FONT_SIZE_LEGIBILITY_THRESHOLD / fontSize, 1), + }; + } + } else { zoomOptions = { fitToContent: true }; } @@ -194,6 +210,7 @@ export const SearchMenu = () => { animate: true, duration: 300, ...zoomOptions, + canvasOffsets: app.getEditorUIOffsets(), }); } } diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index a3b259e366..aa02cc1453 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -223,7 +223,6 @@ export const newTextElement = ( verticalAlign?: VerticalAlign; containerId?: ExcalidrawTextContainer["id"] | null; lineHeight?: ExcalidrawTextElement["lineHeight"]; - strokeWidth?: ExcalidrawTextElement["strokeWidth"]; autoResize?: ExcalidrawTextElement["autoResize"]; } & ElementConstructorOpts, ): NonDeleted => { diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts index b10f31f321..f633789a9d 100644 --- a/packages/excalidraw/element/sizeHelpers.ts +++ b/packages/excalidraw/element/sizeHelpers.ts @@ -2,7 +2,7 @@ import type { ElementsMap, ExcalidrawElement } from "./types"; import { mutateElement } from "./mutateElement"; import { isFreeDrawElement, isLinearElement } from "./typeChecks"; import { SHIFT_LOCKING_ANGLE } from "../constants"; -import type { AppState, Zoom } from "../types"; +import type { AppState, Offsets, Zoom } from "../types"; import { getCommonBounds, getElementBounds } from "./bounds"; import { viewportCoordsToSceneCoords } from "../utils"; @@ -67,12 +67,7 @@ export const isElementCompletelyInViewport = ( scrollY: number; }, elementsMap: ElementsMap, - padding?: Partial<{ - top: number; - right: number; - bottom: number; - left: number; - }>, + padding?: Offsets, ) => { const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates const topLeftSceneCoords = viewportCoordsToSceneCoords( diff --git a/packages/excalidraw/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts index f3d6ac0147..5d059e5b44 100644 --- a/packages/excalidraw/scene/scroll.ts +++ b/packages/excalidraw/scene/scroll.ts @@ -1,4 +1,4 @@ -import type { AppState, PointerCoords, Zoom } from "../types"; +import type { AppState, Offsets, PointerCoords, Zoom } from "../types"; import type { ExcalidrawElement } from "../element/types"; import { getCommonBounds, @@ -31,14 +31,28 @@ export const centerScrollOn = ({ scenePoint, viewportDimensions, zoom, + offsets, }: { scenePoint: PointerCoords; viewportDimensions: { height: number; width: number }; zoom: Zoom; + offsets?: Offsets; }) => { + let scrollX = + (viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value - + scenePoint.x; + + scrollX += (offsets?.left ?? 0) / 2 / zoom.value; + + let scrollY = + (viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value - + scenePoint.y; + + scrollY += (offsets?.top ?? 0) / 2 / zoom.value; + return { - scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x, - scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y, + scrollX, + scrollY, }; }; diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 642ff0e73f..c4ebd994e0 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -851,3 +851,10 @@ export type GenerateDiagramToCode = (props: { frame: ExcalidrawMagicFrameElement; children: readonly ExcalidrawElement[]; }) => MaybePromise<{ html: string }>; + +export type Offsets = Partial<{ + top: number; + right: number; + bottom: number; + left: number; +}>; diff --git a/packages/math/utils.ts b/packages/math/utils.ts index f4d90704f8..bbdf61d8d6 100644 --- a/packages/math/utils.ts +++ b/packages/math/utils.ts @@ -1,14 +1,27 @@ export const PRECISION = 10e-5; -export function clamp(value: number, min: number, max: number) { +export const clamp = (value: number, min: number, max: number) => { return Math.min(Math.max(value, min), max); -} +}; -export function round(value: number, precision: number) { +export const round = ( + value: number, + precision: number, + func: "round" | "floor" | "ceil" = "round", +) => { const multiplier = Math.pow(10, precision); - return Math.round((value + Number.EPSILON) * multiplier) / multiplier; -} + return Math[func]((value + Number.EPSILON) * multiplier) / multiplier; +}; + +export const roundToStep = ( + value: number, + step: number, + func: "round" | "floor" | "ceil" = "round", +): number => { + const factor = 1 / step; + return Math[func](value * factor) / factor; +}; export const average = (a: number, b: number) => (a + b) / 2; From 813f9b702e2b17cfb3c1e1577b921a599f6ad81c Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:26:01 +0200 Subject: [PATCH 08/26] feat: merge search sidebar back to default sidebar (#8497) --- excalidraw-app/data/LocalData.ts | 10 ++++- .../actions/actionToggleSearchMenu.ts | 10 +++-- .../excalidraw/components/DefaultSidebar.tsx | 45 ++++++++++--------- packages/excalidraw/components/HintViewer.tsx | 5 ++- packages/excalidraw/components/LayerUI.tsx | 33 ++++++-------- .../excalidraw/components/SearchSidebar.tsx | 29 ------------ packages/excalidraw/constants.ts | 5 +-- packages/excalidraw/locales/en.json | 2 +- packages/excalidraw/tests/search.test.tsx | 11 +++-- 9 files changed, 65 insertions(+), 85 deletions(-) delete mode 100644 packages/excalidraw/components/SearchSidebar.tsx diff --git a/excalidraw-app/data/LocalData.ts b/excalidraw-app/data/LocalData.ts index df753c89b6..c8ac5b19a4 100644 --- a/excalidraw-app/data/LocalData.ts +++ b/excalidraw-app/data/LocalData.ts @@ -20,7 +20,10 @@ import { get, } from "idb-keyval"; import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; -import { SEARCH_SIDEBAR } from "../../packages/excalidraw/constants"; +import { + CANVAS_SEARCH_TAB, + DEFAULT_SIDEBAR, +} from "../../packages/excalidraw/constants"; import type { LibraryPersistedData } from "../../packages/excalidraw/data/library"; import type { ImportedDataState } from "../../packages/excalidraw/data/types"; import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; @@ -69,7 +72,10 @@ const saveDataStateToLocalStorage = ( try { const _appState = clearAppStateForLocalStorage(appState); - if (_appState.openSidebar?.name === SEARCH_SIDEBAR.name) { + if ( + _appState.openSidebar?.name === DEFAULT_SIDEBAR.name && + _appState.openSidebar.tab === CANVAS_SEARCH_TAB + ) { _appState.openSidebar = null; } diff --git a/packages/excalidraw/actions/actionToggleSearchMenu.ts b/packages/excalidraw/actions/actionToggleSearchMenu.ts index 6072fd30c9..02a58cd2b4 100644 --- a/packages/excalidraw/actions/actionToggleSearchMenu.ts +++ b/packages/excalidraw/actions/actionToggleSearchMenu.ts @@ -3,7 +3,7 @@ import { register } from "./register"; import type { AppState } from "../types"; import { searchIcon } from "../components/icons"; import { StoreAction } from "../store"; -import { CLASSES, SEARCH_SIDEBAR } from "../constants"; +import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants"; export const actionToggleSearchMenu = register({ name: "searchMenu", @@ -17,7 +17,10 @@ export const actionToggleSearchMenu = register({ predicate: (appState) => appState.gridModeEnabled, }, perform(elements, appState, _, app) { - if (appState.openSidebar?.name === SEARCH_SIDEBAR.name) { + if ( + appState.openSidebar?.name === DEFAULT_SIDEBAR.name && + appState.openSidebar.tab === CANVAS_SEARCH_TAB + ) { const searchInput = app.excalidrawContainerValue.container?.querySelector( `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, @@ -31,13 +34,14 @@ export const actionToggleSearchMenu = register({ } searchInput?.focus(); + searchInput?.select(); return false; } return { appState: { ...appState, - openSidebar: { name: SEARCH_SIDEBAR.name }, + openSidebar: { name: DEFAULT_SIDEBAR.name, tab: CANVAS_SEARCH_TAB }, openDialog: null, }, storeAction: StoreAction.NONE, diff --git a/packages/excalidraw/components/DefaultSidebar.tsx b/packages/excalidraw/components/DefaultSidebar.tsx index 5cd588933a..5053a143eb 100644 --- a/packages/excalidraw/components/DefaultSidebar.tsx +++ b/packages/excalidraw/components/DefaultSidebar.tsx @@ -1,5 +1,9 @@ import clsx from "clsx"; -import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants"; +import { + CANVAS_SEARCH_TAB, + DEFAULT_SIDEBAR, + LIBRARY_SIDEBAR_TAB, +} from "../constants"; import { useTunnels } from "../context/tunnels"; import { useUIAppState } from "../context/ui-appState"; import type { MarkOptional, Merge } from "../utility-types"; @@ -10,7 +14,8 @@ import { LibraryMenu } from "./LibraryMenu"; import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common"; import { Sidebar } from "./Sidebar/Sidebar"; import "../components/dropdownMenu/DropdownMenu.scss"; -import { t } from "../i18n"; +import { SearchMenu } from "./SearchMenu"; +import { LibraryIcon, searchIcon } from "./icons"; const DefaultSidebarTrigger = withInternalFallback( "DefaultSidebarTrigger", @@ -66,16 +71,20 @@ export const DefaultSidebar = Object.assign( const { DefaultSidebarTabTriggersTunnel } = useTunnels(); + const isForceDocked = appState.openSidebar?.tab === CANVAS_SEARCH_TAB; + return ( { @@ -85,26 +94,22 @@ export const DefaultSidebar = Object.assign( > - {rest.__fallback && ( -
- {t("toolBar.library")} -
- )} - + + + {searchIcon} + + + {LibraryIcon} + + + {rest.__fallback && }
+ + + {children}
diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx index 7c3431c58a..934ff90050 100644 --- a/packages/excalidraw/components/HintViewer.tsx +++ b/packages/excalidraw/components/HintViewer.tsx @@ -13,7 +13,7 @@ import { isEraserActive } from "../appState"; import "./HintViewer.scss"; import { isNodeInFlowchart } from "../element/flowchart"; import { isGridModeEnabled } from "../snapping"; -import { SEARCH_SIDEBAR } from "../constants"; +import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "../constants"; interface HintViewerProps { appState: UIAppState; @@ -32,7 +32,8 @@ const getHints = ({ const multiMode = appState.multiElement !== null; if ( - appState.openSidebar?.name === SEARCH_SIDEBAR.name && + appState.openSidebar?.name === DEFAULT_SIDEBAR.name && + appState.openSidebar.tab === CANVAS_SEARCH_TAB && appState.searchMatches?.length ) { return t("hints.dismissSearch"); diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index 01d5fd8214..64c34dd1ca 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -5,7 +5,6 @@ import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH, - SEARCH_SIDEBAR, TOOL_TYPE, } from "../constants"; import { showSelectedShapeActions } from "../element"; @@ -54,9 +53,6 @@ import { LibraryIcon } from "./icons"; import { UIAppStateContext } from "../context/ui-appState"; import { DefaultSidebar } from "./DefaultSidebar"; import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper"; - -import "./LayerUI.scss"; -import "./Toolbar.scss"; import { mutateElement } from "../element/mutateElement"; import { ShapeCache } from "../scene/ShapeCache"; import Scene from "../scene/Scene"; @@ -64,7 +60,9 @@ import { LaserPointerButton } from "./LaserPointerButton"; import { TTDDialog } from "./TTDDialog/TTDDialog"; import { Stats } from "./Stats"; import { actionToggleStats } from "../actions"; -import { SearchSidebar } from "./SearchSidebar"; + +import "./LayerUI.scss"; +import "./Toolbar.scss"; interface LayerUIProps { actionManager: ActionManager; @@ -365,21 +363,16 @@ const LayerUI = ({ const renderSidebars = () => { return ( - <> - {appState.openSidebar?.name === SEARCH_SIDEBAR.name && ( - - )} - { - trackEvent( - "sidebar", - `toggleDock (${docked ? "dock" : "undock"})`, - `(${device.editor.isMobile ? "mobile" : "desktop"})`, - ); - }} - /> - + { + trackEvent( + "sidebar", + `toggleDock (${docked ? "dock" : "undock"})`, + `(${device.editor.isMobile ? "mobile" : "desktop"})`, + ); + }} + /> ); }; diff --git a/packages/excalidraw/components/SearchSidebar.tsx b/packages/excalidraw/components/SearchSidebar.tsx deleted file mode 100644 index 7cb93ac5f0..0000000000 --- a/packages/excalidraw/components/SearchSidebar.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { SEARCH_SIDEBAR } from "../constants"; -import { t } from "../i18n"; -import { SearchMenu } from "./SearchMenu"; -import { Sidebar } from "./Sidebar/Sidebar"; - -export const SearchSidebar = () => { - return ( - - - -
- {t("search.title")} -
-
- -
-
- ); -}; diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index 31982d4fbb..d43847b79c 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -377,16 +377,13 @@ export const DEFAULT_ELEMENT_PROPS: { }; export const LIBRARY_SIDEBAR_TAB = "library"; +export const CANVAS_SEARCH_TAB = "search"; export const DEFAULT_SIDEBAR = { name: "default", defaultTab: LIBRARY_SIDEBAR_TAB, } as const; -export const SEARCH_SIDEBAR = { - name: "search", -}; - export const LIBRARY_DISABLED_TYPES = new Set([ "iframe", "embeddable", diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index ff1fa20263..e4c5eea449 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -167,7 +167,7 @@ "noMatch": "No matches found...", "singleResult": "result", "multipleResults": "results", - "placeholder": "Find text..." + "placeholder": "Find text on canvas..." }, "buttons": { "clearReset": "Reset the canvas", diff --git a/packages/excalidraw/tests/search.test.tsx b/packages/excalidraw/tests/search.test.tsx index ae729b2101..68ad658262 100644 --- a/packages/excalidraw/tests/search.test.tsx +++ b/packages/excalidraw/tests/search.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { act, render, waitFor } from "./test-utils"; import { Excalidraw } from "../index"; -import { CLASSES, SEARCH_SIDEBAR } from "../constants"; +import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants"; import { Keyboard } from "./helpers/ui"; import { KEYS } from "../keys"; import { updateTextEditor } from "./queries/dom"; @@ -34,7 +34,8 @@ describe("search", () => { Keyboard.keyPress(KEYS.F); }); expect(h.app.state.openSidebar).not.toBeNull(); - expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); const searchInput = await querySearchInput(); expect(searchInput.matches(":focus")).toBe(true); @@ -78,7 +79,8 @@ describe("search", () => { Keyboard.keyPress(KEYS.F); }); expect(h.app.state.openSidebar).not.toBeNull(); - expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); const searchInput = await querySearchInput(); @@ -122,7 +124,8 @@ describe("search", () => { Keyboard.keyPress(KEYS.F); }); expect(h.app.state.openSidebar).not.toBeNull(); - expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); const searchInput = await querySearchInput(); From 01e83cc9a5eff6af3f7c1ecdca12060a2ca68df2 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:19:14 +0200 Subject: [PATCH 09/26] fix: default sidebar triggers & behavior (#8498) --- .../excalidraw/components/DefaultSidebar.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/excalidraw/components/DefaultSidebar.tsx b/packages/excalidraw/components/DefaultSidebar.tsx index 5053a143eb..70b0c2d6c5 100644 --- a/packages/excalidraw/components/DefaultSidebar.tsx +++ b/packages/excalidraw/components/DefaultSidebar.tsx @@ -37,14 +37,11 @@ const DefaultSidebarTrigger = withInternalFallback( ); DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger"; -const DefaultTabTriggers = ({ - children, - ...rest -}: { children: React.ReactNode } & React.HTMLAttributes) => { +const DefaultTabTriggers = ({ children }: { children: React.ReactNode }) => { const { DefaultSidebarTabTriggersTunnel } = useTunnels(); return ( - {children} + {children} ); }; @@ -76,7 +73,8 @@ export const DefaultSidebar = Object.assign( return ( - + {searchIcon} {LibraryIcon} - - {rest.__fallback && } + + From dc812bee194dc1a07fc3151f2ccb415669c507fa Mon Sep 17 00:00:00 2001 From: hocino <94870540+hocino@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:50:18 +0200 Subject: [PATCH 10/26] docs: replace dead link (#8494) * docs update dead link on main-menu page * doc: fix dead link --- dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx index 5c075de86d..6337fe7ac3 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx @@ -20,7 +20,7 @@ exportToCanvas({
  getDimensions,
  files,
  exportPadding?: number;
-}: ExportOpts +}: ExportOpts | Name | Type | Default | Description | From 80f3b75d42c87a8d3e33fef977753af3d658a419 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:56:45 +0200 Subject: [PATCH 11/26] chore: revert vite 5.4.2 -> 5.0.12 (#8499) --- package.json | 2 +- vitest.config.mts | 2 +- yarn.lock | 300 +++++----------------------------------------- 3 files changed, 30 insertions(+), 274 deletions(-) diff --git a/package.json b/package.json index be906c8407..e5f90eef51 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "prettier": "2.6.2", "rewire": "6.0.0", "typescript": "4.9.4", - "vite": "5.4.2", + "vite": "5.0.12", "vite-plugin-checker": "0.7.2", "vite-plugin-ejs": "1.7.0", "vite-plugin-pwa": "0.17.4", diff --git a/vitest.config.mts b/vitest.config.mts index 6702e6a61a..5e7485fb9a 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -8,7 +8,7 @@ export default defineConfig({ // Since hooks are running in stack in v2, which means all hooks run serially whereas // we need to run them in parallel sequence: { - hooks: 'parallel', + hooks: "parallel", }, setupFiles: ["./setupTests.ts"], globals: true, diff --git a/yarn.lock b/yarn.lock index 9bc3c589e1..ffdcab4e22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1519,11 +1519,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - "@esbuild/android-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz#ef31015416dd79398082409b77aaaa2ade4d531a" @@ -1539,11 +1534,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - "@esbuild/android-arm@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.10.tgz#1c23c7e75473aae9fb323be5d9db225142f47f52" @@ -1559,11 +1549,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - "@esbuild/android-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.10.tgz#df6a4e6d6eb8da5595cfce16d4e3f6bc24464707" @@ -1579,11 +1564,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - "@esbuild/darwin-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz#8462a55db07c1b2fad61c8244ce04469ef1043be" @@ -1599,11 +1579,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - "@esbuild/darwin-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz#d1de20bfd41bb75b955ba86a6b1004539e8218c1" @@ -1619,11 +1594,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - "@esbuild/freebsd-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz#16904879e34c53a2e039d1284695d2db3e664d57" @@ -1639,11 +1609,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - "@esbuild/freebsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz#8ad9e5ca9786ca3f1ef1411bfd10b08dcd9d4cef" @@ -1659,11 +1624,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - "@esbuild/linux-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz#d82cf2c590faece82d28bbf1cfbe36f22ae25bd2" @@ -1679,11 +1639,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - "@esbuild/linux-arm@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz#477b8e7c7bcd34369717b04dd9ee6972c84f4029" @@ -1699,11 +1654,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - "@esbuild/linux-ia32@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz#d55ff822cf5b0252a57112f86857ff23be6cab0e" @@ -1719,11 +1669,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - "@esbuild/linux-loong64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz#a9ad057d7e48d6c9f62ff50f6f208e331c4543c7" @@ -1739,11 +1684,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - "@esbuild/linux-mips64el@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz#b011a96924773d60ebab396fbd7a08de66668179" @@ -1759,11 +1699,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - "@esbuild/linux-ppc64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz#5d8b59929c029811e473f2544790ea11d588d4dd" @@ -1779,11 +1714,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - "@esbuild/linux-riscv64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz#292b06978375b271bd8bc0a554e0822957508d22" @@ -1799,11 +1729,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - "@esbuild/linux-s390x@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz#d30af63530f8d4fa96930374c9dd0d62bf59e069" @@ -1819,11 +1744,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - "@esbuild/linux-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz#898c72eeb74d9f2fb43acf316125b475548b75ce" @@ -1839,11 +1759,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - "@esbuild/netbsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz#fd473a5ae261b43eab6dad4dbd5a3155906e6c91" @@ -1859,11 +1774,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - "@esbuild/openbsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz#96eb8992e526717b5272321eaad3e21f3a608e46" @@ -1879,11 +1789,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - "@esbuild/sunos-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz#c16ee1c167f903eaaa6acf7372bee42d5a89c9bc" @@ -1899,11 +1804,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - "@esbuild/win32-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz#7e417d1971dbc7e469b4eceb6a5d1d667b5e3dcc" @@ -1919,11 +1819,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - "@esbuild/win32-ia32@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz#2b52dfec6cd061ecb36171c13bae554888b439e5" @@ -1939,11 +1834,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - "@esbuild/win32-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz#bd123a74f243d2f3a1f046447bb9b363ee25d072" @@ -1959,11 +1849,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2856,161 +2741,81 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== -"@rollup/rollup-android-arm-eabi@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz#0412834dc423d1ff7be4cb1fc13a86a0cd262c11" - integrity sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg== - "@rollup/rollup-android-arm64@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== -"@rollup/rollup-android-arm64@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz#baf1a014b13654f3b9e835388df9caf8c35389cb" - integrity sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA== - "@rollup/rollup-darwin-arm64@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== -"@rollup/rollup-darwin-arm64@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz#0a2c364e775acdf1172fe3327662eec7c46e55b1" - integrity sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q== - "@rollup/rollup-darwin-x64@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== -"@rollup/rollup-darwin-x64@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz#a972db75890dfab8df0da228c28993220a468c42" - integrity sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w== - "@rollup/rollup-linux-arm-gnueabihf@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== -"@rollup/rollup-linux-arm-gnueabihf@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz#1609d0630ef61109dd19a278353e5176d92e30a1" - integrity sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w== - "@rollup/rollup-linux-arm-musleabihf@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== -"@rollup/rollup-linux-arm-musleabihf@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz#3c1dca5f160aa2e79e4b20ff6395eab21804f266" - integrity sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w== - "@rollup/rollup-linux-arm64-gnu@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== -"@rollup/rollup-linux-arm64-gnu@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz#c2fe376e8b04eafb52a286668a8df7c761470ac7" - integrity sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw== - "@rollup/rollup-linux-arm64-musl@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== -"@rollup/rollup-linux-arm64-musl@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz#e62a4235f01e0f66dbba587c087ca6db8008ec80" - integrity sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w== - "@rollup/rollup-linux-powerpc64le-gnu@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== -"@rollup/rollup-linux-powerpc64le-gnu@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz#24b3457e75ee9ae5b1c198bd39eea53222a74e54" - integrity sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ== - "@rollup/rollup-linux-riscv64-gnu@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== -"@rollup/rollup-linux-riscv64-gnu@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz#38edfba9620fe2ca8116c97e02bd9f2d606bde09" - integrity sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg== - "@rollup/rollup-linux-s390x-gnu@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== -"@rollup/rollup-linux-s390x-gnu@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz#a3bfb8bc5f1e802f8c76cff4a4be2e9f9ac36a18" - integrity sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ== - "@rollup/rollup-linux-x64-gnu@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== -"@rollup/rollup-linux-x64-gnu@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz#0dadf34be9199fcdda44b5985a086326344f30ad" - integrity sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw== - "@rollup/rollup-linux-x64-musl@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== -"@rollup/rollup-linux-x64-musl@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz#7b7deddce240400eb87f2406a445061b4fed99a8" - integrity sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg== - "@rollup/rollup-win32-arm64-msvc@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== -"@rollup/rollup-win32-arm64-msvc@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz#a0ca0c5149c2cfb26fab32e6ba3f16996fbdb504" - integrity sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ== - "@rollup/rollup-win32-ia32-msvc@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== -"@rollup/rollup-win32-ia32-msvc@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz#aae2886beec3024203dbb5569db3a137bc385f8e" - integrity sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw== - "@rollup/rollup-win32-x64-msvc@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== -"@rollup/rollup-win32-x64-msvc@4.21.2": - version "4.21.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz#e4291e3c1bc637083f87936c333cdbcad22af63b" - integrity sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA== - "@rushstack/eslint-patch@^1.1.0": version "1.10.3" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20" @@ -5999,35 +5804,6 @@ esbuild@^0.20.1: "@esbuild/win32-ia32" "0.20.2" "@esbuild/win32-x64" "0.20.2" -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - escalade@^3.1.1, escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -8991,15 +8767,6 @@ postcss@^8.4.32, postcss@^8.4.38, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.2.0" -postcss@^8.4.41: - version "8.4.43" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.43.tgz#a5ddf22f4cc38e64c6ae030182b43e539d316419" - integrity sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.1" - source-map-js "^1.2.0" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9475,31 +9242,6 @@ rollup@^4.13.0, rollup@^4.2.0: "@rollup/rollup-win32-x64-msvc" "4.18.0" fsevents "~2.3.2" -rollup@^4.20.0: - version "4.21.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7" - integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw== - dependencies: - "@types/estree" "1.0.5" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.21.2" - "@rollup/rollup-android-arm64" "4.21.2" - "@rollup/rollup-darwin-arm64" "4.21.2" - "@rollup/rollup-darwin-x64" "4.21.2" - "@rollup/rollup-linux-arm-gnueabihf" "4.21.2" - "@rollup/rollup-linux-arm-musleabihf" "4.21.2" - "@rollup/rollup-linux-arm64-gnu" "4.21.2" - "@rollup/rollup-linux-arm64-musl" "4.21.2" - "@rollup/rollup-linux-powerpc64le-gnu" "4.21.2" - "@rollup/rollup-linux-riscv64-gnu" "4.21.2" - "@rollup/rollup-linux-s390x-gnu" "4.21.2" - "@rollup/rollup-linux-x64-gnu" "4.21.2" - "@rollup/rollup-linux-x64-musl" "4.21.2" - "@rollup/rollup-win32-arm64-msvc" "4.21.2" - "@rollup/rollup-win32-ia32-msvc" "4.21.2" - "@rollup/rollup-win32-x64-msvc" "4.21.2" - fsevents "~2.3.2" - roughjs@4.6.4: version "4.6.4" resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.4.tgz#b6f39b44645854a6e0a4a28b078368701eb7f939" @@ -9925,7 +9667,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9943,6 +9685,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10014,7 +9765,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10695,17 +10453,6 @@ vite@5.0.12: optionalDependencies: fsevents "~2.3.3" -vite@5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.2.tgz#8acb6ec4bfab823cdfc1cb2d6c53ed311bc4e47e" - integrity sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA== - dependencies: - esbuild "^0.21.3" - postcss "^8.4.41" - rollup "^4.20.0" - optionalDependencies: - fsevents "~2.3.3" - vite@^5.0.0: version "5.2.11" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" @@ -11262,7 +11009,7 @@ workbox-window@7.1.0, workbox-window@^7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.1.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11280,6 +11027,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 60e3801691decef03399f9d3012b9032fb39cf20 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 12 Sep 2024 13:42:39 +0200 Subject: [PATCH 12/26] fix: WYSIWYG editor padding is not normalized with zoom.value (#8481) --- packages/excalidraw/element/textWysiwyg.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 2281a0cc33..23778cb7b5 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -247,7 +247,7 @@ export const textWysiwyg = ({ // adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari) const padding = !isSafari - ? Math.ceil(updatedTextElement.fontSize / 2) + ? Math.ceil(updatedTextElement.fontSize / appState.zoom.value / 2) : 0; // Make sure text editor height doesn't go beyond viewport From caf2db934c9f64bdc7055b3f528953f3ba55ee2f Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 12 Sep 2024 14:11:08 +0200 Subject: [PATCH 13/26] fix: aspect ratio of distorted images are not preserved in SVG exports (#8061) --- packages/excalidraw/renderer/staticSvgScene.ts | 1 + packages/excalidraw/tests/__snapshots__/export.test.tsx.snap | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index 19169d4a95..f0bf989670 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -421,6 +421,7 @@ const renderElementToSvg = ( image.setAttribute("width", "100%"); image.setAttribute("height", "100%"); image.setAttribute("href", fileData.dataURL); + image.setAttribute("preserveAspectRatio", "none"); symbol.appendChild(image); diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index cc2e6fa7be..8bcdb9c4ee 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`export > exporting svg containing transformed images > svg export output 1`] = ` -" +" From d4900e8f19307e2e8643a394cf50716435cff75c Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 12 Sep 2024 14:59:38 +0200 Subject: [PATCH 14/26] fix: Linear element complete button disabled (#8492) --- packages/excalidraw/actions/actionFinalize.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f19ab981f5..f8c80e52e8 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -217,6 +217,7 @@ export const actionFinalize = register({ onClick={updateData} visible={appState.multiElement != null} size={data?.size || "medium"} + style={{ pointerEvents: "all" }} /> ), }); From c1b310c56b72f06212ba9f7e74bdfe3214a6a4dd Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 12 Sep 2024 15:48:47 +0200 Subject: [PATCH 15/26] fix: Buffer dependency (#8474) * fix Buffer dependency * moved to encode.ts * move base64 parsing out --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/data/encode.ts | 9 +++++++++ packages/excalidraw/fonts/ExcalidrawFont.ts | 11 ++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/excalidraw/data/encode.ts b/packages/excalidraw/data/encode.ts index 1d5a59556c..104ab1ca88 100644 --- a/packages/excalidraw/data/encode.ts +++ b/packages/excalidraw/data/encode.ts @@ -57,6 +57,15 @@ export const base64ToString = async (base64: string, isByteString = false) => { : byteStringToString(window.atob(base64)); }; +export const base64ToArrayBuffer = (base64: string): ArrayBuffer => { + if (typeof Buffer !== "undefined") { + // Node.js environment + return Buffer.from(base64, "base64").buffer; + } + // Browser environment + return byteStringToArrayBuffer(atob(base64)); +}; + // ----------------------------------------------------------------------------- // text encoding // ----------------------------------------------------------------------------- diff --git a/packages/excalidraw/fonts/ExcalidrawFont.ts b/packages/excalidraw/fonts/ExcalidrawFont.ts index 682ae7394b..51d6578c6a 100644 --- a/packages/excalidraw/fonts/ExcalidrawFont.ts +++ b/packages/excalidraw/fonts/ExcalidrawFont.ts @@ -1,4 +1,8 @@ -import { stringToBase64, toByteString } from "../data/encode"; +import { + base64ToArrayBuffer, + stringToBase64, + toByteString, +} from "../data/encode"; import { LOCAL_FONT_PROTOCOL } from "./metadata"; import loadWoff2 from "./wasm/woff2.loader"; import loadHbSubset from "./wasm/hb-subset.loader"; @@ -49,10 +53,7 @@ export class ExcalidrawFont implements Font { // it's dataurl (server), the font is inlined as base64, no need to fetch if (url.protocol === "data:") { - const arrayBuffer = Buffer.from( - url.toString().split(",")[1], - "base64", - ).buffer; + const arrayBuffer = base64ToArrayBuffer(url.toString().split(",")[1]); const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints( arrayBuffer, From 508f16dc044f9f747ad48f8fad2280753862c85b Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:56:32 +0200 Subject: [PATCH 16/26] refactor: rename example `App.tsx` -> `ExampleApp.tsx` (#8501) --- examples/excalidraw/components/{App.scss => ExampleApp.scss} | 0 examples/excalidraw/components/{App.tsx => ExampleApp.tsx} | 4 ++-- examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx | 2 +- examples/excalidraw/with-script-in-browser/index.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename examples/excalidraw/components/{App.scss => ExampleApp.scss} (100%) rename examples/excalidraw/components/{App.tsx => ExampleApp.tsx} (99%) diff --git a/examples/excalidraw/components/App.scss b/examples/excalidraw/components/ExampleApp.scss similarity index 100% rename from examples/excalidraw/components/App.scss rename to examples/excalidraw/components/ExampleApp.scss diff --git a/examples/excalidraw/components/App.tsx b/examples/excalidraw/components/ExampleApp.tsx similarity index 99% rename from examples/excalidraw/components/App.tsx rename to examples/excalidraw/components/ExampleApp.tsx index 7cfd8a05ac..1e296786ef 100644 --- a/examples/excalidraw/components/App.tsx +++ b/examples/excalidraw/components/ExampleApp.tsx @@ -40,7 +40,7 @@ import type { } from "@excalidraw/excalidraw/dist/excalidraw/element/types"; import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types"; -import "./App.scss"; +import "./ExampleApp.scss"; type Comment = { x: number; @@ -73,7 +73,7 @@ export interface AppProps { excalidrawLib: typeof TExcalidraw; } -export default function App({ +export default function ExampleApp({ appTitle, useCustom, customArgs, diff --git a/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx index 40af9f0cce..e9fa3bb230 100644 --- a/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx +++ b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx @@ -1,7 +1,7 @@ "use client"; import * as excalidrawLib from "@excalidraw/excalidraw"; import { Excalidraw } from "@excalidraw/excalidraw"; -import App from "../../components/App"; +import App from "../../components/ExampleApp"; import "@excalidraw/excalidraw/index.css"; diff --git a/examples/excalidraw/with-script-in-browser/index.tsx b/examples/excalidraw/with-script-in-browser/index.tsx index e8584d7ca7..00daaddc88 100644 --- a/examples/excalidraw/with-script-in-browser/index.tsx +++ b/examples/excalidraw/with-script-in-browser/index.tsx @@ -1,4 +1,4 @@ -import App from "../components/App"; +import App from "../components/ExampleApp"; import React, { StrictMode } from "react"; import { createRoot } from "react-dom/client"; From c07f5a0c80fd3fdd86724d734082beb3b52f7818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Tue, 17 Sep 2024 10:11:07 +0200 Subject: [PATCH 17/26] feat: Common elbow mid segments (#8440) Common start or end segment length for elbow arrows regardless of arrowhead is present --- excalidraw-app/App.tsx | 7 +++- excalidraw-app/components/DebugCanvas.tsx | 14 ++++++-- packages/excalidraw/element/routing.test.tsx | 25 ++++++------- packages/excalidraw/element/routing.ts | 38 +++++++++++--------- packages/excalidraw/visualdebug.ts | 6 ++-- 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index 0076eead11..9b7eadff84 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -649,7 +649,12 @@ const ExcalidrawWrapper = () => { // Render the debug scene if the debug canvas is available if (debugCanvasRef.current && excalidrawAPI) { - debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio); + debugRenderer( + debugCanvasRef.current, + appState, + window.devicePixelRatio, + () => forceRefresh((prev) => !prev), + ); } }; diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index b610ab7b50..471167989c 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -68,12 +68,17 @@ const _debugRenderer = ( canvas: HTMLCanvasElement, appState: AppState, scale: number, + refresh: () => void, ) => { const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( canvas, scale, ); + if (appState.height !== canvas.height || appState.width !== canvas.width) { + refresh(); + } + const context = bootstrapCanvas({ canvas, scale, @@ -138,8 +143,13 @@ export const saveDebugState = (debug: { enabled: boolean }) => { }; export const debugRenderer = throttleRAF( - (canvas: HTMLCanvasElement, appState: AppState, scale: number) => { - _debugRenderer(canvas, appState, scale); + ( + canvas: HTMLCanvasElement, + appState: AppState, + scale: number, + refresh: () => void, + ) => { + _debugRenderer(canvas, appState, scale, refresh); }, { trailing: true }, ); diff --git a/packages/excalidraw/element/routing.test.tsx b/packages/excalidraw/element/routing.test.tsx index 9381541a54..e451fae5d2 100644 --- a/packages/excalidraw/element/routing.test.tsx +++ b/packages/excalidraw/element/routing.test.tsx @@ -94,7 +94,16 @@ describe("elbow arrow routing", () => { describe("elbow arrow ui", () => { beforeEach(async () => { + localStorage.clear(); await render(); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByTestId(contextMenu!, "stats")!); }); it("can follow bound shapes", async () => { @@ -130,8 +139,8 @@ describe("elbow arrow ui", () => { expect(arrow.elbowed).toBe(true); expect(arrow.points).toEqual([ [0, 0], - [35, 0], - [35, 200], + [45, 0], + [45, 200], [90, 200], ]); }); @@ -163,14 +172,6 @@ describe("elbow arrow ui", () => { h.state, )[0] as ExcalidrawArrowElement; - fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { - button: 2, - clientX: 1, - clientY: 1, - }); - const contextMenu = UI.queryContextMenu(); - fireEvent.click(queryByTestId(contextMenu!, "stats")!); - mouse.click(51, 51); const inputAngle = UI.queryStatsProperty("A")?.querySelector( @@ -182,8 +183,8 @@ describe("elbow arrow ui", () => { [0, 0], [35, 0], [35, 90], - [25, 90], - [25, 165], + [35, 90], // Note that coordinates are rounded above! + [35, 165], [103, 165], ]); }); diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index 07f62ca82a..4ac621a6e1 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -235,6 +235,8 @@ export const mutateElbowArrow = ( BASE_PADDING, ), boundsOverlap, + hoveredStartElement && aabbForElement(hoveredStartElement), + hoveredEndElement && aabbForElement(hoveredEndElement), ); const startDonglePosition = getDonglePosition( dynamicAABBs[0], @@ -475,7 +477,11 @@ const generateDynamicAABBs = ( startDifference?: [number, number, number, number], endDifference?: [number, number, number, number], disableSideHack?: boolean, + startElementBounds?: Bounds | null, + endElementBounds?: Bounds | null, ): Bounds[] => { + const startEl = startElementBounds ?? a; + const endEl = endElementBounds ?? b; const [startUp, startRight, startDown, startLeft] = startDifference ?? [ 0, 0, 0, 0, ]; @@ -484,29 +490,29 @@ const generateDynamicAABBs = ( const first = [ a[0] > b[2] ? a[1] > b[3] || a[3] < b[1] - ? Math.min((a[0] + b[2]) / 2, a[0] - startLeft) - : (a[0] + b[2]) / 2 + ? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft) + : (startEl[0] + endEl[2]) / 2 : a[0] > b[0] ? a[0] - startLeft : common[0] - startLeft, a[1] > b[3] ? a[0] > b[2] || a[2] < b[0] - ? Math.min((a[1] + b[3]) / 2, a[1] - startUp) - : (a[1] + b[3]) / 2 + ? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp) + : (startEl[1] + endEl[3]) / 2 : a[1] > b[1] ? a[1] - startUp : common[1] - startUp, a[2] < b[0] ? a[1] > b[3] || a[3] < b[1] - ? Math.max((a[2] + b[0]) / 2, a[2] + startRight) - : (a[2] + b[0]) / 2 + ? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight) + : (startEl[2] + endEl[0]) / 2 : a[2] < b[2] ? a[2] + startRight : common[2] + startRight, a[3] < b[1] ? a[0] > b[2] || a[2] < b[0] - ? Math.max((a[3] + b[1]) / 2, a[3] + startDown) - : (a[3] + b[1]) / 2 + ? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown) + : (startEl[3] + endEl[1]) / 2 : a[3] < b[3] ? a[3] + startDown : common[3] + startDown, @@ -514,29 +520,29 @@ const generateDynamicAABBs = ( const second = [ b[0] > a[2] ? b[1] > a[3] || b[3] < a[1] - ? Math.min((b[0] + a[2]) / 2, b[0] - endLeft) - : (b[0] + a[2]) / 2 + ? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft) + : (endEl[0] + startEl[2]) / 2 : b[0] > a[0] ? b[0] - endLeft : common[0] - endLeft, b[1] > a[3] ? b[0] > a[2] || b[2] < a[0] - ? Math.min((b[1] + a[3]) / 2, b[1] - endUp) - : (b[1] + a[3]) / 2 + ? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp) + : (endEl[1] + startEl[3]) / 2 : b[1] > a[1] ? b[1] - endUp : common[1] - endUp, b[2] < a[0] ? b[1] > a[3] || b[3] < a[1] - ? Math.max((b[2] + a[0]) / 2, b[2] + endRight) - : (b[2] + a[0]) / 2 + ? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight) + : (endEl[2] + startEl[0]) / 2 : b[2] < a[2] ? b[2] + endRight : common[2] + endRight, b[3] < a[1] ? b[0] > a[2] || b[2] < a[0] - ? Math.max((b[3] + a[1]) / 2, b[3] + endDown) - : (b[3] + a[1]) / 2 + ? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown) + : (endEl[3] + startEl[1]) / 2 : b[3] < a[3] ? b[3] + endDown : common[3] + endDown, diff --git a/packages/excalidraw/visualdebug.ts b/packages/excalidraw/visualdebug.ts index 7181719f7c..86f4d39a82 100644 --- a/packages/excalidraw/visualdebug.ts +++ b/packages/excalidraw/visualdebug.ts @@ -110,8 +110,8 @@ export const debugDrawBoundingBox = ( export const debugDrawBounds = ( box: Bounds | Bounds[], opts?: { - color: string; - permanent: boolean; + color?: string; + permanent?: boolean; }, ) => { (isBounds(box) ? [box] : box).forEach((bbox) => @@ -136,7 +136,7 @@ export const debugDrawBounds = ( ], { color: opts?.color ?? "green", - permanent: opts?.permanent, + permanent: !!opts?.permanent, }, ), ); From e0a22edfbd88d499047f00ef81f920465263333a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Tue, 17 Sep 2024 12:20:40 +0200 Subject: [PATCH 18/26] fix: Re-route elbow arrows when pasted (#8448) Re-route elbow arrows when pasted --- packages/excalidraw/components/App.tsx | 20 +++++++++-- packages/excalidraw/element/routing.ts | 48 ++++++++++++++++++++------ 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index fb4ac27b12..a98974f414 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -435,7 +435,7 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize"; import { getVisibleSceneBounds } from "../element/bounds"; import { isMaybeMermaidDefinition } from "../mermaid"; import NewElementCanvas from "./canvases/NewElementCanvas"; -import { mutateElbowArrow } from "../element/routing"; +import { mutateElbowArrow, updateElbowArrow } from "../element/routing"; import { FlowChartCreator, FlowChartNavigator, @@ -3109,7 +3109,23 @@ class App extends React.Component { retainSeed?: boolean; fitToContent?: boolean; }) => { - const elements = restoreElements(opts.elements, null, undefined); + let elements = opts.elements.map((el) => + isElbowArrow(el) + ? { + ...el, + ...updateElbowArrow( + { + ...el, + startBinding: null, + endBinding: null, + }, + this.scene.getNonDeletedElementsMap(), + [el.points[0], el.points[el.points.length - 1]], + ), + } + : el, + ); + elements = restoreElements(elements, null, undefined); const [minX, minY, maxX, maxY] = getCommonBounds(elements); const elementsCenterX = distance(minX, maxX) / 2; diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index 4ac621a6e1..ac11cddb41 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -36,6 +36,7 @@ import { HEADING_UP, vectorToHeading, } from "./heading"; +import type { ElementUpdate } from "./mutateElement"; import { mutateElement } from "./mutateElement"; import { isBindableElement, isRectanguloidElement } from "./typeChecks"; import type { @@ -82,6 +83,39 @@ export const mutateElbowArrow = ( informMutation?: boolean; }, ) => { + const update = updateElbowArrow( + arrow, + elementsMap, + nextPoints, + offset, + options, + ); + if (update) { + mutateElement( + arrow, + { + ...otherUpdates, + ...update, + angle: 0 as Radians, + }, + options?.informMutation, + ); + } else { + console.error("Elbow arrow cannot find a route"); + } +}; + +export const updateElbowArrow = ( + arrow: ExcalidrawElbowArrowElement, + elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, + nextPoints: readonly LocalPoint[], + offset?: Vector, + options?: { + isDragging?: boolean; + disableBinding?: boolean; + informMutation?: boolean; + }, +): ElementUpdate | null => { const origStartGlobalPoint: GlobalPoint = pointTranslate( pointTranslate( nextPoints[0], @@ -297,18 +331,10 @@ export const mutateElbowArrow = ( startDongle && points.unshift(startGlobalPoint); endDongle && points.push(endGlobalPoint); - mutateElement( - arrow, - { - ...otherUpdates, - ...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0), - angle: 0 as Radians, - }, - options?.informMutation, - ); - } else { - console.error("Elbow arrow cannot find a route"); + return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0); } + + return null; }; const offsetFromHeading = ( From 44a1c8d8573c26ba5cb3d63a8e97ca9699971d2d Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:20:22 +0200 Subject: [PATCH 19/26] fix: svg and png frame clipping cases (#8515) --- packages/excalidraw/scene/export.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index b120d0cc9e..6d1b963fcc 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -185,6 +185,11 @@ export const exportToCanvas = async ( exportingFrame ?? null, appState.frameRendering ?? null, ); + // for canvas export, don't clip if exporting a specific frame as it would + // clip the corners of the content + if (exportingFrame) { + frameRendering.clip = false; + } const elementsForRender = prepareElementsForRender({ elements, @@ -351,6 +356,11 @@ export const exportToSvg = async ( }) rotate(${frame.angle} ${cx} ${cy})" width="${frame.width}" height="${frame.height}" + ${ + exportingFrame + ? "" + : `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}` + } > `; From f3f0ab7c8362750593ccc65a973d06b62925be35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Thu, 19 Sep 2024 08:47:23 +0200 Subject: [PATCH 20/26] fix: Elbow arrow fixedpoint flipping now properly flips on inverted resize and flip action (#8324) * Flipping action now properly mirrors selections with elbow arrows * Flipping action now re-centers the selection to the original center to avoid "walking" selections on repeated flipping --- .../excalidraw/actions/actionFlip.test.tsx | 89 +++++++++++++++++++ packages/excalidraw/actions/actionFlip.ts | 51 ++++++++++- .../excalidraw/actions/actionProperties.tsx | 13 --- packages/excalidraw/components/App.tsx | 54 +++++++---- packages/excalidraw/data/restore.ts | 14 +-- packages/excalidraw/element/binding.ts | 5 +- packages/excalidraw/element/dragElements.ts | 9 +- .../excalidraw/element/linearElementEditor.ts | 6 +- packages/excalidraw/element/resizeElements.ts | 18 +--- packages/excalidraw/element/routing.ts | 10 +-- packages/excalidraw/element/typeChecks.ts | 7 +- packages/excalidraw/element/types.ts | 19 ++-- .../excalidraw/renderer/interactiveScene.ts | 2 - .../regressionTests.test.tsx.snap | 4 + packages/excalidraw/tests/helpers/api.ts | 6 +- packages/excalidraw/tests/history.test.tsx | 9 +- packages/excalidraw/tests/resize.test.tsx | 59 +++++++++++- 17 files changed, 290 insertions(+), 85 deletions(-) create mode 100644 packages/excalidraw/actions/actionFlip.test.tsx diff --git a/packages/excalidraw/actions/actionFlip.test.tsx b/packages/excalidraw/actions/actionFlip.test.tsx new file mode 100644 index 0000000000..0a1b9f41d5 --- /dev/null +++ b/packages/excalidraw/actions/actionFlip.test.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { Excalidraw } from "../index"; +import { render } from "../tests/test-utils"; +import { API } from "../tests/helpers/api"; +import { point } from "../../math"; +import { actionFlipHorizontal } from "./actionFlip"; + +const { h } = window; + +const testElements = [ + API.createElement({ + type: "rectangle", + id: "rec1", + x: 1046, + y: 541, + width: 100, + height: 100, + boundElements: [ + { + id: "arr", + type: "arrow", + }, + ], + }), + API.createElement({ + type: "rectangle", + id: "rec2", + x: 1169, + y: 777, + width: 102, + height: 115, + boundElements: [ + { + id: "arr", + type: "arrow", + }, + ], + }), + API.createElement({ + type: "arrow", + id: "arrow", + x: 1103.0717787616313, + y: 536.8531862198708, + width: 159.68539325842903, + height: 333.0396003698186, + startBinding: { + elementId: "rec1", + focus: 0.1366906474820229, + gap: 5.000000000000057, + fixedPoint: [0.5683453237410123, -0.05014327585315258], + }, + endBinding: { + elementId: "rec2", + focus: 0.0014925373134265828, + gap: 5, + fixedPoint: [-0.04862325174825108, 0.4992537313432874], + }, + points: [ + point(0, 0), + point(0, -35), + point(-97.80898876404626, -35), + point(-97.80898876404626, 298.0396003698186), + point(61.87640449438277, 298.0396003698186), + ], + elbowed: true, + }), +]; + +describe("flipping action", () => { + it("flip re-centers the selection even after multiple flip actions", async () => { + await render(); + + API.setSelectedElements(testElements); + + expect(Object.keys(h.state.selectedElementIds).length).toBe(3); + + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); + + const rec1 = h.elements.find((el) => el.id === "rec1"); + expect(rec1?.x).toBeCloseTo(1113.78, 0); + expect(rec1?.y).toBeCloseTo(541, 0); + + const rec2 = h.elements.find((el) => el.id === "rec2"); + expect(rec2?.x).toBeCloseTo(988.72, 0); + expect(rec2?.y).toBeCloseTo(777, 0); + }); +}); diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index a6dad249fb..45ca7a298c 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -2,6 +2,7 @@ import { register } from "./register"; import { getSelectedElements } from "../scene"; import { getNonDeletedElements } from "../element"; import type { + ExcalidrawElbowArrowElement, ExcalidrawElement, NonDeleted, NonDeletedSceneElementsMap, @@ -18,7 +19,9 @@ import { import { updateFrameMembershipOfSelectedElements } from "../frame"; import { flipHorizontal, flipVertical } from "../components/icons"; import { StoreAction } from "../store"; -import { isLinearElement } from "../element/typeChecks"; +import { isElbowArrow, isLinearElement } from "../element/typeChecks"; +import { mutateElbowArrow } from "../element/routing"; +import { mutateElement } from "../element/mutateElement"; export const actionFlipHorizontal = register({ name: "flipHorizontal", @@ -109,7 +112,8 @@ const flipElements = ( flipDirection: "horizontal" | "vertical", app: AppClassProperties, ): ExcalidrawElement[] => { - const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements); + const { minX, minY, maxX, maxY, midX, midY } = + getCommonBoundingBox(selectedElements); resizeMultipleElements( elementsMap, @@ -131,5 +135,48 @@ const flipElements = ( [], ); + // --------------------------------------------------------------------------- + // flipping arrow elements (and potentially other) makes the selection group + // "move" across the canvas because of how arrows can bump against the "wall" + // of the selection, so we need to center the group back to the original + // position so that repeated flips don't accumulate the offset + + const { elbowArrows, otherElements } = selectedElements.reduce( + ( + acc: { + elbowArrows: ExcalidrawElbowArrowElement[]; + otherElements: ExcalidrawElement[]; + }, + element, + ) => + isElbowArrow(element) + ? { ...acc, elbowArrows: acc.elbowArrows.concat(element) } + : { ...acc, otherElements: acc.otherElements.concat(element) }, + { elbowArrows: [], otherElements: [] }, + ); + + const { midX: newMidX, midY: newMidY } = + getCommonBoundingBox(selectedElements); + const [diffX, diffY] = [midX - newMidX, midY - newMidY]; + otherElements.forEach((element) => + mutateElement(element, { + x: element.x + diffX, + y: element.y + diffY, + }), + ); + elbowArrows.forEach((element) => + mutateElbowArrow( + element, + elementsMap, + element.points, + undefined, + undefined, + { + informMutation: false, + }, + ), + ); + // --------------------------------------------------------------------------- + return selectedElements; }; diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 0fa705f23f..92fa329473 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1685,19 +1685,6 @@ export const actionChangeArrowType = register({ : {}), }, ); - } else { - mutateElement( - newElement, - { - startBinding: newElement.startBinding - ? { ...newElement.startBinding, fixedPoint: null } - : null, - endBinding: newElement.endBinding - ? { ...newElement.endBinding, fixedPoint: null } - : null, - }, - false, - ); } return newElement; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index a98974f414..08ad13fa54 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -185,6 +185,7 @@ import type { MagicGenerationData, ExcalidrawNonSelectionElement, ExcalidrawArrowElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { getCenter, getDistance } from "../gesture"; import { @@ -287,6 +288,7 @@ import { getDateTime, isShallowEqual, arrayToMap, + toBrandedType, } from "../utils"; import { createSrcDoc, @@ -3109,22 +3111,44 @@ class App extends React.Component { retainSeed?: boolean; fitToContent?: boolean; }) => { - let elements = opts.elements.map((el) => - isElbowArrow(el) - ? { - ...el, - ...updateElbowArrow( - { - ...el, - startBinding: null, - endBinding: null, - }, - this.scene.getNonDeletedElementsMap(), - [el.points[0], el.points[el.points.length - 1]], + let elements = opts.elements.map((el, _, elements) => { + if (isElbowArrow(el)) { + const startEndElements = [ + el.startBinding && + elements.find((l) => l.id === el.startBinding?.elementId), + el.endBinding && + elements.find((l) => l.id === el.endBinding?.elementId), + ]; + const startBinding = startEndElements[0] ? el.startBinding : null; + const endBinding = startEndElements[1] ? el.endBinding : null; + return { + ...el, + ...updateElbowArrow( + { + ...el, + startBinding, + endBinding, + }, + toBrandedType( + new Map( + startEndElements + .filter((x) => x != null) + .map( + (el) => + [el!.id, el] as [ + string, + Ordered, + ], + ), + ), ), - } - : el, - ); + [el.points[0], el.points[el.points.length - 1]], + ), + }; + } + + return el; + }); elements = restoreElements(elements, null, undefined); const [minX, minY, maxX, maxY] = getCommonBounds(elements); diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 62652066fa..b476995da1 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -5,6 +5,7 @@ import type { ExcalidrawLinearElement, ExcalidrawSelectionElement, ExcalidrawTextElement, + FixedPointBinding, FontFamilyValues, OrderedExcalidrawElement, PointBinding, @@ -21,6 +22,7 @@ import { import { isArrowElement, isElbowArrow, + isFixedPointBinding, isLinearElement, isTextElement, isUsingAdaptiveRadius, @@ -101,8 +103,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => { const repairBinding = ( element: ExcalidrawLinearElement, - binding: PointBinding | null, -): PointBinding | null => { + binding: PointBinding | FixedPointBinding | null, +): PointBinding | FixedPointBinding | null => { if (!binding) { return null; } @@ -110,9 +112,11 @@ const repairBinding = ( return { ...binding, focus: binding.focus || 0, - fixedPoint: isElbowArrow(element) - ? normalizeFixedPoint(binding.fixedPoint ?? [0, 0]) - : null, + ...(isElbowArrow(element) && isFixedPointBinding(binding) + ? { + fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]), + } + : {}), }; }; diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index fe820723f4..62e66f645b 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -39,6 +39,7 @@ import { isBindingElement, isBoundToContainer, isElbowArrow, + isFixedPointBinding, isFrameLikeElement, isLinearElement, isRectangularElement, @@ -797,7 +798,7 @@ export const bindPointToSnapToElementOutline = ( isVertical ? Math.abs(p[1] - i[1]) < 0.1 : Math.abs(p[0] - i[0]) < 0.1, - )[0] ?? point; + )[0] ?? p; } return p; @@ -1013,7 +1014,7 @@ const updateBoundPoint = ( const direction = startOrEnd === "startBinding" ? -1 : 1; const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1; - if (isElbowArrow(linearElement)) { + if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) { const fixedPoint = normalizeFixedPoint(binding.fixedPoint) ?? calculateFixedPointForElbowArrowBinding( diff --git a/packages/excalidraw/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts index 18d78fdbef..5775f0eb74 100644 --- a/packages/excalidraw/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -35,7 +35,6 @@ export const dragSelectedElements = ( ) => { if ( _selectedElements.length === 1 && - isArrowElement(_selectedElements[0]) && isElbowArrow(_selectedElements[0]) && (_selectedElements[0].startBinding || _selectedElements[0].endBinding) ) { @@ -43,13 +42,7 @@ export const dragSelectedElements = ( } const selectedElements = _selectedElements.filter( - (el) => - !( - isArrowElement(el) && - isElbowArrow(el) && - el.startBinding && - el.endBinding - ), + (el) => !(isElbowArrow(el) && el.startBinding && el.endBinding), ); // we do not want a frame and its elements to be selected at the same time diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index 7607a2e162..e11c0b158c 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -102,6 +102,7 @@ export class LinearElementEditor { public readonly endBindingElement: ExcalidrawBindableElement | null | "keep"; public readonly hoverPointIndex: number; public readonly segmentMidPointHoveredCoords: GlobalPoint | null; + public readonly elbowed: boolean; constructor(element: NonDeleted) { this.elementId = element.id as string & { @@ -131,6 +132,7 @@ export class LinearElementEditor { }; this.hoverPointIndex = -1; this.segmentMidPointHoveredCoords = null; + this.elbowed = isElbowArrow(element) && element.elbowed; } // --------------------------------------------------------------------------- @@ -1477,7 +1479,9 @@ export class LinearElementEditor { nextPoints, vector(offsetX, offsetY), bindings, - options, + { + isDragging: options?.isDragging, + }, ); } else { const nextCoords = getElementPointsCoords(element, nextPoints); diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 3f3f8ef1e2..0a01459e69 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -9,6 +9,7 @@ import type { ExcalidrawTextElementWithContainer, ExcalidrawImageElement, ElementsMap, + ExcalidrawArrowElement, NonDeletedSceneElementsMap, SceneElementsMap, } from "./types"; @@ -909,6 +910,8 @@ export const resizeMultipleElements = ( fontSize?: ExcalidrawTextElement["fontSize"]; scale?: ExcalidrawImageElement["scale"]; boundTextFontSize?: ExcalidrawTextElement["fontSize"]; + startBinding?: ExcalidrawArrowElement["startBinding"]; + endBinding?: ExcalidrawArrowElement["endBinding"]; }; }[] = []; @@ -993,19 +996,6 @@ export const resizeMultipleElements = ( mutateElement(element, update, false); - if (isArrowElement(element) && isElbowArrow(element)) { - mutateElbowArrow( - element, - elementsMap, - element.points, - undefined, - undefined, - { - informMutation: false, - }, - ); - } - updateBoundElements(element, elementsMap, { simultaneouslyUpdated: elementsToUpdate, oldSize: { width: oldWidth, height: oldHeight }, @@ -1059,7 +1049,7 @@ const rotateMultipleElements = ( (centerAngle + origAngle - element.angle) as Radians, ); - if (isArrowElement(element) && isElbowArrow(element)) { + if (isElbowArrow(element)) { const points = getArrowLocalFixedPoints(element, elementsMap); mutateElbowArrow(element, elementsMap, points); } else { diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index ac11cddb41..895340c91a 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -41,7 +41,6 @@ import { mutateElement } from "./mutateElement"; import { isBindableElement, isRectanguloidElement } from "./typeChecks"; import type { ExcalidrawElbowArrowElement, - FixedPointBinding, NonDeletedSceneElementsMap, SceneElementsMap, } from "./types"; @@ -73,13 +72,12 @@ export const mutateElbowArrow = ( elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, nextPoints: readonly LocalPoint[], offset?: Vector, - otherUpdates?: { - startBinding?: FixedPointBinding | null; - endBinding?: FixedPointBinding | null; - }, + otherUpdates?: Omit< + ElementUpdate, + "angle" | "x" | "y" | "width" | "height" | "elbowed" | "points" + >, options?: { isDragging?: boolean; - disableBinding?: boolean; informMutation?: boolean; }, ) => { diff --git a/packages/excalidraw/element/typeChecks.ts b/packages/excalidraw/element/typeChecks.ts index 5ba089ab01..6bb4269f87 100644 --- a/packages/excalidraw/element/typeChecks.ts +++ b/packages/excalidraw/element/typeChecks.ts @@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = ( }; export const isFixedPointBinding = ( - binding: PointBinding, + binding: PointBinding | FixedPointBinding, ): binding is FixedPointBinding => { - return binding.fixedPoint != null; + return ( + Object.hasOwn(binding, "fixedPoint") && + (binding as FixedPointBinding).fixedPoint != null + ); }; // TODO: Move this to @excalidraw/math diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 9b09254276..5ebf505444 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -193,6 +193,7 @@ export type ExcalidrawElement = | ExcalidrawGenericElement | ExcalidrawTextElement | ExcalidrawLinearElement + | ExcalidrawArrowElement | ExcalidrawFreeDrawElement | ExcalidrawImageElement | ExcalidrawFrameElement @@ -268,15 +269,19 @@ export type PointBinding = { elementId: ExcalidrawBindableElement["id"]; focus: number; gap: number; - // Represents the fixed point binding information in form of a vertical and - // horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio - // gives the user selected fixed point by multiplying the bound element width - // with fixedPoint[0] and the bound element height with fixedPoint[1] to get the - // bound element-local point coordinate. - fixedPoint: FixedPoint | null; }; -export type FixedPointBinding = Merge; +export type FixedPointBinding = Merge< + PointBinding, + { + // Represents the fixed point binding information in form of a vertical and + // horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio + // gives the user selected fixed point by multiplying the bound element width + // with fixedPoint[0] and the bound element height with fixedPoint[1] to get the + // bound element-local point coordinate. + fixedPoint: FixedPoint; + } +>; export type Arrowhead = | "arrow" diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 0d03b0f5a0..7dc84db996 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -52,7 +52,6 @@ import { } from "./helpers"; import oc from "open-color"; import { - isArrowElement, isElbowArrow, isFrameLikeElement, isLinearElement, @@ -807,7 +806,6 @@ const _renderInteractiveScene = ({ // Elbow arrow elements cannot be selected when bound on either end ( isSingleLinearElementSelected && - isArrowElement(element) && isElbowArrow(element) && (element.startBinding || element.endBinding) ) diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index c4683267c9..6e1f535036 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -8430,6 +8430,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "elbowed": false, "elementId": "id0", "endBindingElement": "keep", "hoverPointIndex": -1, @@ -8649,6 +8650,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "elbowed": false, "elementId": "id0", "endBindingElement": "keep", "hoverPointIndex": -1, @@ -9058,6 +9060,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "elbowed": false, "elementId": "id0", "endBindingElement": "keep", "hoverPointIndex": -1, @@ -9454,6 +9457,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "elbowed": false, "elementId": "id0", "endBindingElement": "keep", "hoverPointIndex": -1, diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 6c16e51909..d98a908f7a 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -9,6 +9,8 @@ import type { ExcalidrawFrameElement, ExcalidrawElementType, ExcalidrawMagicFrameElement, + ExcalidrawElbowArrowElement, + ExcalidrawArrowElement, } from "../../element/types"; import { newElement, newTextElement, newLinearElement } from "../../element"; import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants"; @@ -179,10 +181,10 @@ export class API { scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never; status?: T extends "image" ? ExcalidrawImageElement["status"] : never; startBinding?: T extends "arrow" - ? ExcalidrawLinearElement["startBinding"] + ? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"] : never; endBinding?: T extends "arrow" - ? ExcalidrawLinearElement["endBinding"] + ? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"] : never; elbowed?: boolean; }): T extends "arrow" | "line" diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 8e825e4148..3c807cf915 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -31,6 +31,7 @@ import type { ExcalidrawGenericElement, ExcalidrawLinearElement, ExcalidrawTextElement, + FixedPointBinding, FractionalIndex, SceneElementsMap, } from "../element/types"; @@ -2049,13 +2050,13 @@ describe("history", () => { focus: -0.001587301587301948, gap: 5, fixedPoint: [1.0318471337579618, 0.49920634920634904], - }, + } as FixedPointBinding, endBinding: { elementId: "u2JGnnmoJ0VATV4vCNJE5", focus: -0.0016129032258049847, gap: 3.537079145500037, fixedPoint: [0.4991935483870975, -0.03875193720914723], - }, + } as FixedPointBinding, }, ], storeAction: StoreAction.CAPTURE, @@ -4455,7 +4456,7 @@ describe("history", () => { elements: [ h.elements[0], newElementWith(h.elements[1], { boundElements: [] }), - newElementWith(h.elements[2] as ExcalidrawLinearElement, { + newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, { endBinding: { elementId: remoteContainer.id, gap: 1, @@ -4655,7 +4656,7 @@ describe("history", () => { // Simulate remote update API.updateScene({ elements: [ - newElementWith(h.elements[0] as ExcalidrawLinearElement, { + newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, { startBinding: { elementId: rect1.id, gap: 1, diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/excalidraw/tests/resize.test.tsx index d18f5cd498..8de7157b18 100644 --- a/packages/excalidraw/tests/resize.test.tsx +++ b/packages/excalidraw/tests/resize.test.tsx @@ -4,6 +4,7 @@ import { render } from "./test-utils"; import { reseed } from "../random"; import { UI, Keyboard, Pointer } from "./helpers/ui"; import type { + ExcalidrawElbowArrowElement, ExcalidrawFreeDrawElement, ExcalidrawLinearElement, } from "../element/types"; @@ -333,6 +334,62 @@ describe("arrow element", () => { expect(label.angle).toBeCloseTo(0); expect(label.fontSize).toEqual(20); }); + + it("flips the fixed point binding on negative resize for single bindable", () => { + const rectangle = UI.createElement("rectangle", { + x: -100, + y: -75, + width: 95, + height: 100, + }); + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + mouse.reset(); + mouse.moveTo(-5, 0); + mouse.click(); + mouse.moveTo(120, 200); + mouse.click(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawElbowArrowElement; + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + + UI.resize(rectangle, "se", [-200, -150]); + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + }); + + it("flips the fixed point binding on negative resize for group selection", () => { + const rectangle = UI.createElement("rectangle", { + x: -100, + y: -75, + width: 95, + height: 100, + }); + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + mouse.reset(); + mouse.moveTo(-5, 0); + mouse.click(); + mouse.moveTo(120, 200); + mouse.click(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawElbowArrowElement; + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + + UI.resize([rectangle, arrow], "nw", [300, 350]); + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25); + }); }); describe("text element", () => { @@ -828,7 +885,6 @@ describe("multiple selection", () => { expect(leftBoundArrow.endBinding?.elementId).toBe( leftArrowBinding.elementId, ); - expect(leftBoundArrow.endBinding?.fixedPoint).toBeNull(); expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus); expect(rightBoundArrow.x).toBeCloseTo(210); @@ -843,7 +899,6 @@ describe("multiple selection", () => { expect(rightBoundArrow.endBinding?.elementId).toBe( rightArrowBinding.elementId, ); - expect(rightBoundArrow.endBinding?.fixedPoint).toBeNull(); expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus); }); From 8ca4cf32605343c1631ed7ef57f0c9d87f4c23b1 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:46:36 +0200 Subject: [PATCH 21/26] feat: flip arrowheads if only arrow(s) selected (#8525) Co-authored-by: Mark Tolmacs --- .../excalidraw/actions/actionFlip.test.tsx | 256 +++++++++++++----- packages/excalidraw/actions/actionFlip.ts | 24 +- packages/excalidraw/tests/helpers/api.ts | 12 + 3 files changed, 223 insertions(+), 69 deletions(-) diff --git a/packages/excalidraw/actions/actionFlip.test.tsx b/packages/excalidraw/actions/actionFlip.test.tsx index 0a1b9f41d5..c8a6239cdf 100644 --- a/packages/excalidraw/actions/actionFlip.test.tsx +++ b/packages/excalidraw/actions/actionFlip.test.tsx @@ -3,87 +3,209 @@ import { Excalidraw } from "../index"; import { render } from "../tests/test-utils"; import { API } from "../tests/helpers/api"; import { point } from "../../math"; -import { actionFlipHorizontal } from "./actionFlip"; +import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip"; const { h } = window; -const testElements = [ - API.createElement({ - type: "rectangle", - id: "rec1", - x: 1046, - y: 541, - width: 100, - height: 100, - boundElements: [ - { - id: "arr", +describe("flipping re-centers selection", () => { + it("elbow arrow touches group selection side yet it remains in place after multiple moves", async () => { + const elements = [ + API.createElement({ + type: "rectangle", + id: "rec1", + x: 100, + y: 100, + width: 100, + height: 100, + boundElements: [{ id: "arr", type: "arrow" }], + }), + API.createElement({ + type: "rectangle", + id: "rec2", + x: 220, + y: 250, + width: 100, + height: 100, + boundElements: [{ id: "arr", type: "arrow" }], + }), + API.createElement({ type: "arrow", - }, - ], - }), - API.createElement({ - type: "rectangle", - id: "rec2", - x: 1169, - y: 777, - width: 102, - height: 115, - boundElements: [ - { id: "arr", - type: "arrow", - }, - ], - }), - API.createElement({ - type: "arrow", - id: "arrow", - x: 1103.0717787616313, - y: 536.8531862198708, - width: 159.68539325842903, - height: 333.0396003698186, - startBinding: { - elementId: "rec1", - focus: 0.1366906474820229, - gap: 5.000000000000057, - fixedPoint: [0.5683453237410123, -0.05014327585315258], - }, - endBinding: { - elementId: "rec2", - focus: 0.0014925373134265828, - gap: 5, - fixedPoint: [-0.04862325174825108, 0.4992537313432874], - }, - points: [ - point(0, 0), - point(0, -35), - point(-97.80898876404626, -35), - point(-97.80898876404626, 298.0396003698186), - point(61.87640449438277, 298.0396003698186), - ], - elbowed: true, - }), -]; - -describe("flipping action", () => { - it("flip re-centers the selection even after multiple flip actions", async () => { - await render(); - - API.setSelectedElements(testElements); + x: 149.9, + y: 95, + width: 156, + height: 239.9, + startBinding: { + elementId: "rec1", + focus: 0, + gap: 5, + fixedPoint: [0.49, -0.05], + }, + endBinding: { + elementId: "rec2", + focus: 0, + gap: 5, + fixedPoint: [-0.05, 0.49], + }, + startArrowhead: null, + endArrowhead: "arrow", + points: [ + point(0, 0), + point(0, -35), + point(-90.9, -35), + point(-90.9, 204.9), + point(65.1, 204.9), + ], + elbowed: true, + }), + ]; + await render(); + + API.setSelectedElements(elements); expect(Object.keys(h.state.selectedElementIds).length).toBe(3); API.executeAction(actionFlipHorizontal); API.executeAction(actionFlipHorizontal); API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); const rec1 = h.elements.find((el) => el.id === "rec1"); - expect(rec1?.x).toBeCloseTo(1113.78, 0); - expect(rec1?.y).toBeCloseTo(541, 0); + expect(rec1?.x).toBeCloseTo(100); + expect(rec1?.y).toBeCloseTo(100); const rec2 = h.elements.find((el) => el.id === "rec2"); - expect(rec2?.x).toBeCloseTo(988.72, 0); - expect(rec2?.y).toBeCloseTo(777, 0); + expect(rec2?.x).toBeCloseTo(220); + expect(rec2?.y).toBeCloseTo(250); + }); +}); + +describe("flipping arrowheads", () => { + beforeEach(async () => { + await render(); + }); + + it("flipping bound arrow should flip arrowheads only", () => { + const rect = API.createElement({ + type: "rectangle", + boundElements: [{ type: "arrow", id: "arrow1" }], + }); + const arrow = API.createElement({ + type: "arrow", + id: "arrow1", + startArrowhead: "arrow", + endArrowhead: null, + endBinding: { + elementId: rect.id, + focus: 0.5, + gap: 5, + }, + }); + + API.setElements([rect, arrow]); + API.setSelectedElements([arrow]); + + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe(null); + + API.executeAction(actionFlipHorizontal); + expect(API.getElement(arrow).startArrowhead).toBe(null); + expect(API.getElement(arrow).endArrowhead).toBe("arrow"); + + API.executeAction(actionFlipHorizontal); + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe(null); + + API.executeAction(actionFlipVertical); + expect(API.getElement(arrow).startArrowhead).toBe(null); + expect(API.getElement(arrow).endArrowhead).toBe("arrow"); + }); + + it("flipping bound arrow should flip arrowheads only 2", () => { + const rect = API.createElement({ + type: "rectangle", + boundElements: [{ type: "arrow", id: "arrow1" }], + }); + const rect2 = API.createElement({ + type: "rectangle", + boundElements: [{ type: "arrow", id: "arrow1" }], + }); + const arrow = API.createElement({ + type: "arrow", + id: "arrow1", + startArrowhead: "arrow", + endArrowhead: "circle", + startBinding: { + elementId: rect.id, + focus: 0.5, + gap: 5, + }, + endBinding: { + elementId: rect2.id, + focus: 0.5, + gap: 5, + }, + }); + + API.setElements([rect, rect2, arrow]); + API.setSelectedElements([arrow]); + + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe("circle"); + + API.executeAction(actionFlipHorizontal); + expect(API.getElement(arrow).startArrowhead).toBe("circle"); + expect(API.getElement(arrow).endArrowhead).toBe("arrow"); + + API.executeAction(actionFlipVertical); + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe("circle"); + }); + + it("flipping unbound arrow shouldn't flip arrowheads", () => { + const arrow = API.createElement({ + type: "arrow", + id: "arrow1", + startArrowhead: "arrow", + endArrowhead: "circle", + }); + + API.setElements([arrow]); + API.setSelectedElements([arrow]); + + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe("circle"); + + API.executeAction(actionFlipHorizontal); + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe("circle"); + }); + + it("flipping bound arrow shouldn't flip arrowheads if selected alongside non-arrow eleemnt", () => { + const rect = API.createElement({ + type: "rectangle", + boundElements: [{ type: "arrow", id: "arrow1" }], + }); + const arrow = API.createElement({ + type: "arrow", + id: "arrow1", + startArrowhead: "arrow", + endArrowhead: null, + endBinding: { + elementId: rect.id, + focus: 0.5, + gap: 5, + }, + }); + + API.setElements([rect, arrow]); + API.setSelectedElements([rect, arrow]); + + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe(null); + + API.executeAction(actionFlipHorizontal); + expect(API.getElement(arrow).startArrowhead).toBe("arrow"); + expect(API.getElement(arrow).endArrowhead).toBe(null); }); }); diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index 45ca7a298c..6b75b8facd 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -2,6 +2,7 @@ import { register } from "./register"; import { getSelectedElements } from "../scene"; import { getNonDeletedElements } from "../element"; import type { + ExcalidrawArrowElement, ExcalidrawElbowArrowElement, ExcalidrawElement, NonDeleted, @@ -19,9 +20,13 @@ import { import { updateFrameMembershipOfSelectedElements } from "../frame"; import { flipHorizontal, flipVertical } from "../components/icons"; import { StoreAction } from "../store"; -import { isElbowArrow, isLinearElement } from "../element/typeChecks"; +import { + isArrowElement, + isElbowArrow, + isLinearElement, +} from "../element/typeChecks"; import { mutateElbowArrow } from "../element/routing"; -import { mutateElement } from "../element/mutateElement"; +import { mutateElement, newElementWith } from "../element/mutateElement"; export const actionFlipHorizontal = register({ name: "flipHorizontal", @@ -112,6 +117,21 @@ const flipElements = ( flipDirection: "horizontal" | "vertical", app: AppClassProperties, ): ExcalidrawElement[] => { + if ( + selectedElements.every( + (element) => + isArrowElement(element) && (element.startBinding || element.endBinding), + ) + ) { + return selectedElements.map((element) => { + const _element = element as ExcalidrawArrowElement; + return newElementWith(_element, { + startArrowhead: _element.endArrowhead, + endArrowhead: _element.startArrowhead, + }); + }); + } + const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(selectedElements); diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index d98a908f7a..b7dc6e10d6 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -129,6 +129,10 @@ export class API { expect(API.getSelectedElements().length).toBe(0); }; + static getElement = (element: T): T => { + return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element; + } + static createElement = < T extends Exclude = "rectangle", >({ @@ -186,6 +190,12 @@ export class API { endBinding?: T extends "arrow" ? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"] : never; + startArrowhead?: T extends "arrow" + ? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"] + : never; + endArrowhead?: T extends "arrow" + ? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"] + : never; elbowed?: boolean; }): T extends "arrow" | "line" ? ExcalidrawLinearElement @@ -342,6 +352,8 @@ export class API { if (element.type === "arrow") { element.startBinding = rest.startBinding ?? null; element.endBinding = rest.endBinding ?? null; + element.startArrowhead = rest.startArrowhead ?? null; + element.endArrowhead = rest.endArrowhead ?? null; } if (id) { element.id = id; From 6dfa18414ac9075ab5f15b6bf2607a9ce8ff68d4 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:01:28 +0200 Subject: [PATCH 22/26] test: decrease min coverage thresholds (#8541) --- vitest.config.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vitest.config.mts b/vitest.config.mts index 5e7485fb9a..1dadf85d29 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -19,10 +19,10 @@ export default defineConfig({ // Additionally the thresholds also needs to be updated slightly as a result of this change ignoreEmptyLines: false, thresholds: { - lines: 66, + lines: 60, branches: 70, functions: 63, - statements: 66, + statements: 60, }, }, }, From a80cb5896aa79e02d5f0cf21e375cae6d6059f0c Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Tue, 24 Sep 2024 17:30:21 +0200 Subject: [PATCH 23/26] feat: self-hosting existing google fonts (#8540) --- excalidraw-app/index.html | 9 --- excalidraw-app/vite.config.mts | 2 + ...r-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 | Bin 0 -> 1416 bytes ...ular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 | Bin 0 -> 10676 bytes ...KofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 | Bin 0 -> 8392 bytes ...1BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 | Bin 0 -> 16476 bytes ...KofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 | Bin 0 -> 11104 bytes ...KofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 | Bin 0 -> 15476 bytes ...KofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 | Bin 0 -> 6116 bytes packages/excalidraw/fonts/index.ts | 16 ++-- .../scene/__snapshots__/export.test.ts.snap | 20 ++--- packages/utils/package.json | 1 - scripts/buildPackage.js | 8 +- scripts/buildUtils.js | 10 +-- scripts/woff2/woff2-esbuild-plugins.js | 65 +--------------- scripts/woff2/woff2-vite-plugins.js | 46 ++++-------- vitest.config.mts | 2 - yarn.lock | 70 +----------------- 18 files changed, 48 insertions(+), 201 deletions(-) create mode 100644 packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 create mode 100644 packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 create mode 100644 packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 create mode 100644 packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 create mode 100644 packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 create mode 100644 packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 create mode 100644 packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 diff --git a/excalidraw-app/index.html b/excalidraw-app/index.html index 7ec74e181b..02f153d8c3 100644 --- a/excalidraw-app/index.html +++ b/excalidraw-app/index.html @@ -130,15 +130,6 @@ <% } %> - - - i00lJw0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c9f8bb01755447ec5a47c6af758c80c094f083fa GIT binary patch literal 16476 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 F3;-%H literal 0 HcmV?d00001 diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6b06971404d8ec80ce520f9306e7095bdf461b98 GIT binary patch literal 11104 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 literal 0 HcmV?d00001 diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c21a6ed084822b3dd76be36de8f0ee169d24c276 GIT binary patch literal 15476 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 literal 0 HcmV?d00001 diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bd56dc50d72819f92f09344cfb50fe3180785dec GIT binary patch literal 6116 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 literal 0 HcmV?d00001 diff --git a/packages/excalidraw/fonts/index.ts b/packages/excalidraw/fonts/index.ts index 39f6bf8da2..1de1f99c9a 100644 --- a/packages/excalidraw/fonts/index.ts +++ b/packages/excalidraw/fonts/index.ts @@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2"; import ComicShanns from "./assets/ComicShanns-Regular.woff2"; import LiberationSans from "./assets/LiberationSans-Regular.woff2"; -import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; -import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; - -import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; -import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; -import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; -import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; -import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; +import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; +import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; + +import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; +import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; +import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; +import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; +import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; export class Fonts { // it's ok to track fonts across multiple instances only once, so let's use diff --git a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap index bab78a8327..a75a36d0b4 100644 --- a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap +++ b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap @@ -27,23 +27,23 @@ exports[`exportToSvg > with default arguments 1`] = ` } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHXAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgacMo33iNXw4rQ+nvxRQyQajY97hEpO8P1+7PfcdxGVaJKIKtmThcIQKqFYJCQypZO+JO2e/tu0qRygmchjGCYuED6gcn65sG8dyxhwDcTreH0G11fptDPt5S7w3wKNOLEs4YAH+gNN/Aef2Dge6GM0H+TWMFnXRIyHyPjw0TxRoFa/PLaydngJERnKEgRqTajzw/N074QN8V2QC3HA8jinpfglSLpPt8B2TRun8hmlSI45/JX/pfj5H3kxJoaxB4gvKYb5hVAtoZ0P43y3lzoxXP/NZiG0CyiEFCMAgYCoiF8AhChLTSBAIpYVFCieB7aWTjUt/64uih/woes7fJ67WFaVy3kiNwOVEgh+SQcAqsYYFRPxSxdDOBZ0IyAAQBaoc68QuQYv/o0MM/5l0uhNFlZ8tTPLCr0eRaUpzUpQbzRg0E5YQWuvNCKGI8jxxnVE8ckgOfaQNzbDhxsKg0ZHjkkuA0FrP4jw5pC6DYN9TSF4d5GL62kaauu2O3PsxLF1nWZ4vq+RbG/EbZinz7eX0NYvQe91tXX0cZd0Cm78/lMCMik+EG5OIjokeLgyHSFbnqmWFo2B6KR3TR+Qo0WDkMamUCEX8bS4j3KFn0Llq34AAA==); + src: url(data:font/woff2;base64,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); } @@ -153,23 +153,23 @@ exports[`exportToSvg > with exportEmbedScene 1`] = ` } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); + src: url(data:font/woff2;base64,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); } @font-face { font-family: Nunito; - src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHXAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgacMo33iNXw4rQ+nvxRQyQajY97hEpO8P1+7PfcdxGVaJKIKtmThcIQKqFYJCQypZO+JO2e/tu0qRygmchjGCYuED6gcn65sG8dyxhwDcTreH0G11fptDPt5S7w3wKNOLEs4YAH+gNN/Aef2Dge6GM0H+TWMFnXRIyHyPjw0TxRoFa/PLaydngJERnKEgRqTajzw/N074QN8V2QC3HA8jinpfglSLpPt8B2TRun8hmlSI45/JX/pfj5H3kxJoaxB4gvKYb5hVAtoZ0P43y3lzoxXP/NZiG0CyiEFCMAgYCoiF8AhChLTSBAIpYVFCieB7aWTjUt/64uih/woes7fJ67WFaVy3kiNwOVEgh+SQcAqsYYFRPxSxdDOBZ0IyAAQBaoc68QuQYv/o0MM/5l0uhNFlZ8tTPLCr0eRaUpzUpQbzRg0E5YQWuvNCKGI8jxxnVE8ckgOfaQNzbDhxsKg0ZHjkkuA0FrP4jw5pC6DYN9TSF4d5GL62kaauu2O3PsxLF1nWZ4vq+RbG/EbZinz7eX0NYvQe91tXX0cZd0Cm78/lMCMik+EG5OIjokeLgyHSFbnqmWFo2B6KR3TR+Qo0WDkMamUCEX8bS4j3KFn0Llq34AAA==); + src: url(data:font/woff2;base64,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); } diff --git a/packages/utils/package.json b/packages/utils/package.json index 0e8650152a..b739eddc74 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -68,7 +68,6 @@ "css-loader": "6.7.1", "file-loader": "6.2.0", "fonteditor-core": "2.4.0", - "node-fetch": "3.3.2", "sass-loader": "13.0.2", "ts-loader": "9.3.1", "typescript": "4.9.4", diff --git a/scripts/buildPackage.js b/scripts/buildPackage.js index 442afaf202..a1699e3d6c 100644 --- a/scripts/buildPackage.js +++ b/scripts/buildPackage.js @@ -1,7 +1,6 @@ const { build } = require("esbuild"); const { sassPlugin } = require("esbuild-sass-plugin"); const { externalGlobalPlugin } = require("esbuild-plugin-external-global"); -const { woff2BrowserPlugin } = require("./woff2/woff2-esbuild-plugins"); // Will be used later for treeshaking //const fs = require("fs"); @@ -45,13 +44,15 @@ const browserConfig = { format: "esm", plugins: [ sassPlugin(), - woff2BrowserPlugin(), externalGlobalPlugin({ react: "React", "react-dom": "ReactDOM", }), ], splitting: true, + loader: { + ".woff2": "file", + }, }; const createESMBrowserBuild = async () => { // Development unminified build with source maps @@ -100,9 +101,10 @@ const rawConfig = { entryPoints: ["index.tsx"], bundle: true, format: "esm", - plugins: [sassPlugin(), woff2BrowserPlugin()], + plugins: [sassPlugin()], loader: { ".json": "copy", + ".woff2": "file", }, packages: "external", }; diff --git a/scripts/buildUtils.js b/scripts/buildUtils.js index 0250b6bc13..269119cbd7 100644 --- a/scripts/buildUtils.js +++ b/scripts/buildUtils.js @@ -1,17 +1,17 @@ const fs = require("fs"); const { build } = require("esbuild"); const { sassPlugin } = require("esbuild-sass-plugin"); -const { - woff2BrowserPlugin, - woff2ServerPlugin, -} = require("./woff2/woff2-esbuild-plugins"); +const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins"); const browserConfig = { entryPoints: ["index.ts"], bundle: true, format: "esm", - plugins: [sassPlugin(), woff2BrowserPlugin()], + plugins: [sassPlugin()], assetNames: "assets/[name]", + loader: { + ".woff2": "file", + }, }; // Will be used later for treeshaking diff --git a/scripts/woff2/woff2-esbuild-plugins.js b/scripts/woff2/woff2-esbuild-plugins.js index b9b4338f65..d332edf30f 100644 --- a/scripts/woff2/woff2-esbuild-plugins.js +++ b/scripts/woff2/woff2-esbuild-plugins.js @@ -2,45 +2,9 @@ const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); const which = require("which"); -const fetch = require("node-fetch"); const wawoff = require("wawoff2"); const { Font } = require("fonteditor-core"); -/** - * Custom esbuild plugin to convert url woff2 imports into a text. - * Other woff2 imports are handled by a "file" loader. - * - * @returns {import("esbuild").Plugin} - */ -module.exports.woff2BrowserPlugin = () => { - return { - name: "woff2BrowserPlugin", - setup(build) { - build.initialOptions.loader = { - ".woff2": "file", - ...build.initialOptions.loader, - }; - - build.onResolve({ filter: /^https:\/\/.+?\.woff2$/ }, (args) => { - return { - path: args.path, - namespace: "woff2BrowserPlugin", - }; - }); - - build.onLoad( - { filter: /.*/, namespace: "woff2BrowserPlugin" }, - async (args) => { - return { - contents: args.path, - loader: "text", - }; - }, - ); - }, - }; -}; - /** * Custom esbuild plugin to: * 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs) @@ -53,27 +17,6 @@ module.exports.woff2BrowserPlugin = () => { * @returns {import("esbuild").Plugin} */ module.exports.woff2ServerPlugin = (options = {}) => { - // google CDN fails time to time, so let's retry - async function fetchRetry(url, options = {}, retries = 0, delay = 1000) { - try { - const response = await fetch(url, options); - - if (!response.ok) { - throw new Error(`Status: ${response.status}, ${await response.json()}`); - } - - return response; - } catch (e) { - if (retries > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)); - return fetchRetry(url, options, retries - 1, delay * 2); - } - - console.error(`Couldn't fetch: ${url}, error: ${e.message}`); - throw e; - } - } - return { name: "woff2ServerPlugin", setup(build) { @@ -82,9 +25,7 @@ module.exports.woff2ServerPlugin = (options = {}) => { const fonts = new Map(); build.onResolve({ filter: /\.woff2$/ }, (args) => { - const resolvedPath = args.path.startsWith("http") - ? args.path // url - : path.resolve(args.resolveDir, args.path); // absolute path + const resolvedPath = path.resolve(args.resolveDir, args.path); return { path: resolvedPath, @@ -101,9 +42,7 @@ module.exports.woff2ServerPlugin = (options = {}) => { // read local woff2 as a buffer (WARN: `readFileSync` does not work!) woff2Buffer = await fs.promises.readFile(args.path); } else { - // fetch remote woff2 as a buffer (i.e. from a cdn) - const response = await fetchRetry(args.path, {}, 3); - woff2Buffer = await response.buffer(); + throw new Error(`Font path has to be absolute! "${args.path}"`); } // google's brotli decompression into snft diff --git a/scripts/woff2/woff2-vite-plugins.js b/scripts/woff2/woff2-vite-plugins.js index f25488e86f..48826b3ce5 100644 --- a/scripts/woff2/woff2-vite-plugins.js +++ b/scripts/woff2/woff2-vite-plugins.js @@ -1,15 +1,13 @@ +// `EXCALIDRAW_ASSET_PATH` as a SSOT const OSS_FONTS_CDN = "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/"; /** - * Custom vite plugin to convert url woff2 imports into a text. - * Other woff2 imports are automatically served and resolved as a file uri. + * Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`. * * @returns {import("vite").PluginOption} */ module.exports.woff2BrowserPlugin = () => { - // for now limited to woff2 only, might be extended to any assets in the future - const regex = /^https:\/\/.+?\.woff2$/; let isDev; return { @@ -18,34 +16,9 @@ module.exports.woff2BrowserPlugin = () => { config(_, { command }) { isDev = command === "serve"; }, - resolveId(source) { - if (!regex.test(source)) { - return null; - } - - // getting the url to the dependency tree - return source; - }, - load(id) { - if (!regex.test(id)) { - return null; - } - - // loading the url as string - return `export default "${id}"`; - }, - // necessary for dev as vite / rollup does skips https imports in serve (~dev) mode - // aka dev mode equivalent of "export default x" above (resolveId + load) transform(code, id) { - // treat https woff2 imports as a text - if (isDev && id.endsWith("/excalidraw/fonts/index.ts")) { - return code.replaceAll( - /import\s+(\w+)\s+from\s+(["']https:\/\/.+?\.woff2["'])/g, - `const $1 = $2`, - ); - } - - // use CDN for Assistant + // using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically), + // but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app` if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) { return `/* WARN: The following content is generated during excalidraw-app build */ @@ -90,7 +63,6 @@ module.exports.woff2BrowserPlugin = () => { }`; } - // using EXCALIDRAW_ASSET_PATH as a SSOT if (!isDev && id.endsWith("excalidraw-app/index.html")) { return code.replace( "", @@ -110,9 +82,10 @@ module.exports.woff2BrowserPlugin = () => { type="font/woff2" crossorigin="anonymous" /> + { type="font/woff2" crossorigin="anonymous" /> + `, ); } diff --git a/vitest.config.mts b/vitest.config.mts index 1dadf85d29..99098eb915 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,9 +1,7 @@ import { defineConfig } from "vitest/config"; -import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins"; export default defineConfig({ //@ts-ignore - plugins: [woff2BrowserPlugin()], test: { // Since hooks are running in stack in v2, which means all hooks run serially whereas // we need to run them in parallel diff --git a/yarn.lock b/yarn.lock index ffdcab4e22..6c022abe32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5194,11 +5194,6 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - data-urls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" @@ -6262,14 +6257,6 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - fflate@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" @@ -6408,13 +6395,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - fraction.js@^4.2.0: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -8256,11 +8236,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -8273,15 +8248,6 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-html-parser@^5.3.3: version "5.4.2" resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a" @@ -9667,7 +9633,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9685,15 +9651,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -9765,14 +9722,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10562,11 +10512,6 @@ wawoff2@2.0.1: dependencies: argparse "^2.0.1" -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - web-worker@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" @@ -11009,7 +10954,7 @@ workbox-window@7.1.0, workbox-window@^7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.1.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11027,15 +10972,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 3fe1883f3f6bc45592f2aa6c721af6694e61935f Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 24 Sep 2024 21:09:15 +0530 Subject: [PATCH 24/26] feat: prefer user defined coords and dimensions over calculated for for frame (#8517) * feat: prefer user defined coords and dimensions over calculated for frame * update changelog * lint * show the info only in dev mode and when children present --- packages/excalidraw/CHANGELOG.md | 2 + .../data/__snapshots__/transform.test.ts.snap | 54 +++++------ packages/excalidraw/data/transform.test.ts | 89 ++++++++++--------- packages/excalidraw/data/transform.ts | 27 ++++-- 4 files changed, 98 insertions(+), 74 deletions(-) diff --git a/packages/excalidraw/CHANGELOG.md b/packages/excalidraw/CHANGELOG.md index 4d7dbe8a51..b4dcb5759d 100644 --- a/packages/excalidraw/CHANGELOG.md +++ b/packages/excalidraw/CHANGELOG.md @@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section. ### Features +- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517) + - `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise`. [#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) diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 921118eb1f..967de923e1 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "backgroundColor": "#d8f5a2", "boundElements": [ { - "id": "id45", + "id": "id47", "type": "arrow", }, { - "id": "id46", + "id": "id48", "type": "arrow", }, ], @@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "backgroundColor": "transparent", "boundElements": [ { - "id": "id46", + "id": "id48", "type": "arrow", }, ], @@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "seed": Any, "startArrowhead": null, "startBinding": { - "elementId": "id47", + "elementId": "id49", "fixedPoint": null, "focus": -0.08139534883720931, "gap": 1, @@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "backgroundColor": "transparent", "boundElements": [ { - "id": "id45", + "id": "id47", "type": "arrow", }, ], @@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "boundElements": [ { - "id": "id48", + "id": "id50", "type": "arrow", }, ], @@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "boundElements": [ { - "id": "id48", + "id": "id50", "type": "arrow", }, ], @@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "backgroundColor": "transparent", "boundElements": [ { - "id": "id49", + "id": "id51", "type": "text", }, ], @@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id48", + "containerId": "id50", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "backgroundColor": "transparent", "boundElements": [ { - "id": "id38", + "id": "id40", "type": "text", }, ], @@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id40", + "elementId": "id42", "fixedPoint": null, "focus": 0, "gap": 1, @@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "seed": Any, "startArrowhead": null, "startBinding": { - "elementId": "id39", + "elementId": "id41", "fixedPoint": null, "focus": 0, "gap": 1, @@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id37", + "containerId": "id39", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "backgroundColor": "transparent", "boundElements": [ { - "id": "id37", + "id": "id39", "type": "arrow", }, ], @@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "backgroundColor": "transparent", "boundElements": [ { - "id": "id37", + "id": "id39", "type": "arrow", }, ], @@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "backgroundColor": "transparent", "boundElements": [ { - "id": "id42", + "id": "id44", "type": "text", }, ], @@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id44", + "elementId": "id46", "fixedPoint": null, "focus": 0, "gap": 1, @@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "seed": Any, "startArrowhead": null, "startBinding": { - "elementId": "id43", + "elementId": "id45", "fixedPoint": null, "focus": 0, "gap": 1, @@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id41", + "containerId": "id43", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "backgroundColor": "transparent", "boundElements": [ { - "id": "id41", + "id": "id43", "type": "arrow", }, ], @@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "backgroundColor": "transparent", "boundElements": [ { - "id": "id41", + "id": "id43", "type": "arrow", }, ], @@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id54", + "id": "id56", "type": "text", }, { @@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id55", + "id": "id57", "type": "text", }, ], @@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id56", + "id": "id58", "type": "text", }, { @@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id57", + "id": "id59", "type": "text", }, { @@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id58", + "id": "id60", "type": "text", }, ], @@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "backgroundColor": "transparent", "boundElements": [ { - "id": "id59", + "id": "id61", "type": "text", }, ], diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts index d930cb923b..d752f6766a 100644 --- a/packages/excalidraw/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -309,28 +309,32 @@ describe("Test Transform", () => { }); describe("Test Frames", () => { + const elements: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + id: "1", + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + id: "2", + }, + ]; + it("should transform frames and update frame ids when regenerated", () => { const elementsSkeleton: ExcalidrawElementSkeleton[] = [ - { - 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", - }, + ...elements, { type: "frame", children: ["1", "2"], @@ -352,28 +356,9 @@ describe("Test Transform", () => { }); }); - it("should consider max of calculated and frame dimensions when provided", () => { + it("should consider user defined frame dimensions over calculated when provided", () => { const elementsSkeleton: 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", - }, + ...elements, { type: "frame", children: ["1", "2"], @@ -388,7 +373,27 @@ describe("Test Transform", () => { ); const frame = excalidrawElements.find((ele) => ele.type === "frame")!; expect(frame.width).toBe(800); - expect(frame.height).toBe(126); + expect(frame.height).toBe(100); + }); + + it("should consider user defined frame coordinates calculated when provided", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + ...elements, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + x: 100, + y: 300, + }, + ]; + const excalidrawElements = convertToExcalidrawElements( + elementsSkeleton, + opts, + ); + const frame = excalidrawElements.find((ele) => ele.type === "frame")!; + expect(frame.x).toBe(100); + expect(frame.y).toBe(300); }); }); diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 6573abd0d0..aa276668bf 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -46,6 +46,7 @@ import { assertNever, cloneJSON, getFontString, + isDevEnv, toBrandedType, } from "../utils"; import { getSizeFromPoints } from "../points"; @@ -717,7 +718,7 @@ export const convertToExcalidrawElements = ( } // Once all the excalidraw elements are created, we can add frames since we - // need to calculate coordinates and dimensions of frame which is possibe after all + // need to calculate coordinates and dimensions of frame which is possible after all // frame children are processed. for (const [id, element] of elementsWithIds) { if (element.type !== "frame" && element.type !== "magicframe") { @@ -764,10 +765,26 @@ export const convertToExcalidrawElements = ( maxX = maxX + PADDING; maxY = maxY + PADDING; - // Take the max of calculated and provided frame dimensions, whichever is higher - const width = Math.max(frame?.width, maxX - minX); - const height = Math.max(frame?.height, maxY - minY); - Object.assign(frame, { x: minX, y: minY, width, height }); + const frameX = frame?.x || minX; + const frameY = frame?.y || minY; + const frameWidth = frame?.width || maxX - minX; + const frameHeight = frame?.height || maxY - minY; + + Object.assign(frame, { + x: frameX, + y: frameY, + width: frameWidth, + height: frameHeight, + }); + if ( + isDevEnv() && + element.children.length && + (frame?.x || frame?.y || frame?.width || frame?.height) + ) { + console.info( + "User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically", + ); + } } return elementStore.getElements(); From a977dd1bf53091830062071889ebf3542d85a772 Mon Sep 17 00:00:00 2001 From: Subhadeep Sengupta <92149645+subhadeep-sengupta@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:49:18 +0530 Subject: [PATCH 25/26] feat: Added reddit links as embeddable (#8099) feat: #8063 Added reddit links as embeddable Co-authored-by: Aakansha Doshi --- packages/excalidraw/element/embeddable.ts | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/excalidraw/element/embeddable.ts b/packages/excalidraw/element/embeddable.ts index e262c79337..eada31a5be 100644 --- a/packages/excalidraw/element/embeddable.ts +++ b/packages/excalidraw/element/embeddable.ts @@ -45,6 +45,12 @@ const RE_GENERIC_EMBED = const RE_GIPHY = /giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/; +const RE_REDDIT = + /^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/; + +const RE_REDDIT_EMBED = + /^ { @@ -218,6 +226,24 @@ export const getEmbedLink = ( return ret; } + if (RE_REDDIT.test(link)) { + const [, page, postId, title] = link.match(RE_REDDIT)!; + const safeURL = sanitizeHTMLAttribute( + `https://reddit.com/r/${page}/comments/${postId}/${title}`, + ); + const ret: IframeDataWithSandbox = { + type: "document", + srcdoc: (theme: string) => + createSrcDoc( + `

`, + ), + intrinsicSize: { w: 480, h: 480 }, + sandbox: { allowSameOrigin }, + }; + embeddedLinkCache.set(originalLink, ret); + return ret; + } + if (RE_GH_GIST.test(link)) { const [, user, gistId] = link.match(RE_GH_GIST)!; const safeURL = sanitizeHTMLAttribute( @@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => { return twitterMatch[1]; } + const redditMatch = str.match(RE_REDDIT_EMBED); + if (redditMatch && redditMatch.length === 2) { + return redditMatch[1]; + } + const gistMatch = str.match(RE_GH_GIST_EMBED); if (gistMatch && gistMatch.length === 2) { return gistMatch[1]; From 47ee8a00945793340bb20715b2f383a4c3da2139 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:27:17 +0200 Subject: [PATCH 26/26] refactor: `point()` -> `pointFrom()` to fix compiler issue (#8578) --- .../excalidraw/actions/actionFinalize.tsx | 4 +- .../excalidraw/actions/actionFlip.test.tsx | 12 +- .../excalidraw/actions/actionProperties.tsx | 4 +- packages/excalidraw/charts.ts | 10 +- packages/excalidraw/components/App.tsx | 64 ++++---- .../components/Stats/MultiDimension.tsx | 6 +- .../components/Stats/MultiPosition.tsx | 18 +- .../excalidraw/components/Stats/Position.tsx | 10 +- .../components/Stats/stats.test.tsx | 26 +-- packages/excalidraw/components/Stats/utils.ts | 10 +- .../components/hyperlink/Hyperlink.tsx | 4 +- .../components/hyperlink/helpers.ts | 13 +- packages/excalidraw/data/restore.ts | 6 +- packages/excalidraw/data/transform.test.ts | 6 +- packages/excalidraw/data/transform.ts | 6 +- packages/excalidraw/element/binding.ts | 75 +++++---- packages/excalidraw/element/bounds.test.ts | 8 +- packages/excalidraw/element/bounds.ts | 92 +++++------ packages/excalidraw/element/collision.ts | 18 +- packages/excalidraw/element/flowchart.ts | 4 +- packages/excalidraw/element/heading.ts | 18 +- .../excalidraw/element/linearElementEditor.ts | 104 ++++++------ .../excalidraw/element/newElement.test.ts | 4 +- packages/excalidraw/element/resizeElements.ts | 96 ++++++----- packages/excalidraw/element/resizeTest.ts | 30 ++-- packages/excalidraw/element/routing.test.tsx | 10 +- packages/excalidraw/element/routing.ts | 10 +- .../excalidraw/element/textWysiwyg.test.tsx | 4 +- .../excalidraw/element/transformHandles.ts | 6 +- packages/excalidraw/frame.ts | 8 +- packages/excalidraw/renderer/renderSnaps.ts | 38 +++-- packages/excalidraw/scene/Shape.ts | 4 +- packages/excalidraw/shapes.tsx | 52 +++--- packages/excalidraw/snapping.ts | 154 ++++++++++-------- packages/excalidraw/tests/binding.test.tsx | 13 +- packages/excalidraw/tests/flip.test.tsx | 8 +- packages/excalidraw/tests/helpers/api.ts | 10 +- packages/excalidraw/tests/helpers/ui.ts | 15 +- packages/excalidraw/tests/history.test.tsx | 18 +- .../tests/linearElementEditor.test.tsx | 85 +++++----- packages/excalidraw/tests/resize.test.tsx | 65 ++++---- packages/excalidraw/visualdebug.ts | 47 +++--- packages/math/arc.test.ts | 8 +- packages/math/curve.ts | 22 +-- packages/math/point.test.ts | 10 +- packages/math/point.ts | 14 +- packages/utils/collision.test.ts | 66 ++++---- packages/utils/collision.ts | 4 +- packages/utils/geometry/geometry.test.ts | 124 +++++++++----- packages/utils/geometry/shape.ts | 85 +++++----- packages/utils/withinBounds.ts | 20 +-- 51 files changed, 845 insertions(+), 703 deletions(-) diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f8c80e52e8..0637e304f0 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -15,7 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks"; import type { AppState } from "../types"; import { resetCursor } from "../cursor"; import { StoreAction } from "../store"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; import { isPathALoop } from "../shapes"; export const actionFinalize = register({ @@ -115,7 +115,7 @@ export const actionFinalize = register({ mutateElement(multiPointElement, { points: linePoints.map((p, index) => index === linePoints.length - 1 - ? point(firstPoint[0], firstPoint[1]) + ? pointFrom(firstPoint[0], firstPoint[1]) : p, ), }); diff --git a/packages/excalidraw/actions/actionFlip.test.tsx b/packages/excalidraw/actions/actionFlip.test.tsx index c8a6239cdf..5ee587b205 100644 --- a/packages/excalidraw/actions/actionFlip.test.tsx +++ b/packages/excalidraw/actions/actionFlip.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Excalidraw } from "../index"; import { render } from "../tests/test-utils"; import { API } from "../tests/helpers/api"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip"; const { h } = window; @@ -50,11 +50,11 @@ describe("flipping re-centers selection", () => { startArrowhead: null, endArrowhead: "arrow", points: [ - point(0, 0), - point(0, -35), - point(-90.9, -35), - point(-90.9, 204.9), - point(65.1, 204.9), + pointFrom(0, 0), + pointFrom(0, -35), + pointFrom(-90.9, -35), + pointFrom(-90.9, 204.9), + pointFrom(65.1, 204.9), ], elbowed: true, }), diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 92fa329473..72ff8896b7 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -116,7 +116,7 @@ import { import { mutateElbowArrow } from "../element/routing"; import { LinearElementEditor } from "../element/linearElementEditor"; import type { LocalPoint } from "../../math"; -import { point, vector } from "../../math"; +import { pointFrom, vector } from "../../math"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; @@ -1651,7 +1651,7 @@ export const actionChangeArrowType = register({ elementsMap, [finalStartPoint, finalEndPoint].map( (p): LocalPoint => - point(p[0] - newElement.x, p[1] - newElement.y), + pointFrom(p[0] - newElement.x, p[1] - newElement.y), ), vector(0, 0), { diff --git a/packages/excalidraw/charts.ts b/packages/excalidraw/charts.ts index 87d2b34fe3..6e379c30a5 100644 --- a/packages/excalidraw/charts.ts +++ b/packages/excalidraw/charts.ts @@ -1,5 +1,5 @@ import type { Radians } from "../math"; -import { point } from "../math"; +import { pointFrom } from "../math"; import { COLOR_PALETTE, DEFAULT_CHART_COLOR_INDEX, @@ -260,7 +260,7 @@ const chartLines = ( x, y, width: chartWidth, - points: [point(0, 0), point(chartWidth, 0)], + points: [pointFrom(0, 0), pointFrom(chartWidth, 0)], }); const yLine = newLinearElement({ @@ -271,7 +271,7 @@ const chartLines = ( x, y, height: chartHeight, - points: [point(0, 0), point(0, -chartHeight)], + points: [pointFrom(0, 0), pointFrom(0, -chartHeight)], }); const maxLine = newLinearElement({ @@ -284,7 +284,7 @@ const chartLines = ( strokeStyle: "dotted", width: chartWidth, opacity: GRID_OPACITY, - points: [point(0, 0), point(chartWidth, 0)], + points: [pointFrom(0, 0), pointFrom(chartWidth, 0)], }); return [xLine, yLine, maxLine]; @@ -441,7 +441,7 @@ const chartTypeLine = ( height: cy, strokeStyle: "dotted", opacity: GRID_OPACITY, - points: [point(0, 0), point(0, cy)], + points: [pointFrom(0, 0), pointFrom(0, cy)], }); }); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 08ad13fa54..8a976dd8bf 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -445,7 +445,7 @@ import { } from "../element/flowchart"; import { searchItemInFocusAtom } from "./SearchMenu"; import type { LocalPoint, Radians } from "../../math"; -import { point, pointDistance, vector } from "../../math"; +import { pointFrom, pointDistance, vector } from "../../math"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -4910,7 +4910,7 @@ class App extends React.Component { this.getElementHitThreshold(), ); - return isPointInShape(point(x, y), selectionShape); + return isPointInShape(pointFrom(x, y), selectionShape); } // take bound text element into consideration for hit collision as well @@ -5269,7 +5269,7 @@ class App extends React.Component { element, this.scene.getNonDeletedElementsMap(), this.state, - point(scenePointer.x, scenePointer.y), + pointFrom(scenePointer.x, scenePointer.y), this.device.editor.isMobile, ) ); @@ -5281,11 +5281,14 @@ class App extends React.Component { isTouchScreen: boolean, ) => { const draggedDistance = pointDistance( - point( + pointFrom( this.lastPointerDownEvent!.clientX, this.lastPointerDownEvent!.clientY, ), - point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY), + pointFrom( + this.lastPointerUpEvent!.clientX, + this.lastPointerUpEvent!.clientY, + ), ); if ( !this.hitLinkElement || @@ -5304,7 +5307,7 @@ class App extends React.Component { this.hitLinkElement, elementsMap, this.state, - point(lastPointerDownCoords.x, lastPointerDownCoords.y), + pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y), this.device.editor.isMobile, ); const lastPointerUpCoords = viewportCoordsToSceneCoords( @@ -5315,7 +5318,7 @@ class App extends React.Component { this.hitLinkElement, elementsMap, this.state, - point(lastPointerUpCoords.x, lastPointerUpCoords.y), + pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y), this.device.editor.isMobile, ); if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { @@ -5565,7 +5568,7 @@ class App extends React.Component { // threshold, add a point if ( pointDistance( - point(scenePointerX - rx, scenePointerY - ry), + pointFrom(scenePointerX - rx, scenePointerY - ry), lastPoint, ) >= LINE_CONFIRM_THRESHOLD ) { @@ -5574,7 +5577,7 @@ class App extends React.Component { { points: [ ...points, - point(scenePointerX - rx, scenePointerY - ry), + pointFrom(scenePointerX - rx, scenePointerY - ry), ], }, false, @@ -5588,7 +5591,7 @@ class App extends React.Component { points.length > 2 && lastCommittedPoint && pointDistance( - point(scenePointerX - rx, scenePointerY - ry), + pointFrom(scenePointerX - rx, scenePointerY - ry), lastCommittedPoint, ) < LINE_CONFIRM_THRESHOLD ) { @@ -5636,7 +5639,7 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), [ ...points.slice(0, -1), - point( + pointFrom( lastCommittedX + dxFromLastCommitted, lastCommittedY + dyFromLastCommitted, ), @@ -5655,7 +5658,7 @@ class App extends React.Component { { points: [ ...points.slice(0, -1), - point( + pointFrom( lastCommittedX + dxFromLastCommitted, lastCommittedY + dyFromLastCommitted, ), @@ -5884,8 +5887,8 @@ class App extends React.Component { }; const distance = pointDistance( - point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y), - point(scenePointer.x, scenePointer.y), + pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y), + pointFrom(scenePointer.x, scenePointer.y), ); const threshold = this.getElementHitThreshold(); const p = { ...pointerDownState.lastCoords }; @@ -6397,7 +6400,7 @@ class App extends React.Component { this.hitLinkElement, this.scene.getNonDeletedElementsMap(), this.state, - point(scenePointer.x, scenePointer.y), + pointFrom(scenePointer.x, scenePointer.y), ) ) { this.handleEmbeddableCenterClick(this.hitLinkElement); @@ -7088,7 +7091,7 @@ class App extends React.Component { simulatePressure, locked: false, frameId: topLayerFrame ? topLayerFrame.id : null, - points: [point(0, 0)], + points: [pointFrom(0, 0)], pressures: simulatePressure ? [] : [event.pressure], }); @@ -7297,7 +7300,10 @@ class App extends React.Component { multiElement.points.length > 1 && lastCommittedPoint && pointDistance( - point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry), + pointFrom( + pointerDownState.origin.x - rx, + pointerDownState.origin.y - ry, + ), lastCommittedPoint, ) < LINE_CONFIRM_THRESHOLD ) { @@ -7399,7 +7405,7 @@ class App extends React.Component { }; }); mutateElement(element, { - points: [...element.points, point(0, 0)], + points: [...element.points, pointFrom(0, 0)], }); const boundElement = getHoveredElementForBinding( pointerDownState.origin, @@ -7652,8 +7658,8 @@ class App extends React.Component { ) { if ( pointDistance( - point(pointerCoords.x, pointerCoords.y), - point(pointerDownState.origin.x, pointerDownState.origin.y), + pointFrom(pointerCoords.x, pointerCoords.y), + pointFrom(pointerDownState.origin.x, pointerDownState.origin.y), ) < DRAGGING_THRESHOLD ) { return; @@ -8002,7 +8008,7 @@ class App extends React.Component { mutateElement( newElement, { - points: [...points, point(dx, dy)], + points: [...points, pointFrom(dx, dy)], pressures, }, false, @@ -8031,7 +8037,7 @@ class App extends React.Component { mutateElement( newElement, { - points: [...points, point(dx, dy)], + points: [...points, pointFrom(dx, dy)], }, false, ); @@ -8039,7 +8045,7 @@ class App extends React.Component { mutateElbowArrow( newElement, elementsMap, - [...points.slice(0, -1), point(dx, dy)], + [...points.slice(0, -1), pointFrom(dx, dy)], vector(0, 0), undefined, { @@ -8051,7 +8057,7 @@ class App extends React.Component { mutateElement( newElement, { - points: [...points.slice(0, -1), point(dx, dy)], + points: [...points.slice(0, -1), pointFrom(dx, dy)], }, false, ); @@ -8360,9 +8366,9 @@ class App extends React.Component { : [...newElement.pressures, childEvent.pressure]; mutateElement(newElement, { - points: [...points, point(dx, dy)], + points: [...points, pointFrom(dx, dy)], pressures, - lastCommittedPoint: point(dx, dy), + lastCommittedPoint: pointFrom(dx, dy), }); this.actionManager.executeAction(actionFinalize); @@ -8409,7 +8415,7 @@ class App extends React.Component { mutateElement(newElement, { points: [ ...newElement.points, - point( + pointFrom( pointerCoords.x - newElement.x, pointerCoords.y - newElement.y, ), @@ -8723,8 +8729,8 @@ class App extends React.Component { this.eraserTrail.endPath(); const draggedDistance = pointDistance( - point(pointerStart.clientX, pointerStart.clientY), - point(pointerEnd.clientX, pointerEnd.clientY), + pointFrom(pointerStart.clientX, pointerStart.clientY), + pointFrom(pointerEnd.clientX, pointerEnd.clientY), ); if (draggedDistance === 0) { diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index f362005855..0d1a65e915 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -20,7 +20,7 @@ import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; import { getElementsInAtomicUnit, resizeElement } from "./utils"; import type { AtomicUnit } from "./utils"; import { MIN_WIDTH_OR_HEIGHT } from "../../constants"; -import { point, type GlobalPoint } from "../../../math"; +import { pointFrom, type GlobalPoint } from "../../../math"; interface MultiDimensionProps { property: "width" | "height"; @@ -182,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, initialHeight, aspectRatio, - point(x1, y1), + pointFrom(x1, y1), property, latestElements, originalElements, @@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType< nextHeight, initialHeight, aspectRatio, - point(x1, y1), + pointFrom(x1, y1), property, latestElements, originalElements, diff --git a/packages/excalidraw/components/Stats/MultiPosition.tsx b/packages/excalidraw/components/Stats/MultiPosition.tsx index d0f001663e..3285faf6ab 100644 --- a/packages/excalidraw/components/Stats/MultiPosition.tsx +++ b/packages/excalidraw/components/Stats/MultiPosition.tsx @@ -13,7 +13,7 @@ import { useMemo } from "react"; import { getElementsInAtomicUnit, moveElement } from "./utils"; import type { AtomicUnit } from "./utils"; import type { AppState } from "../../types"; -import { point, pointRotateRads } from "../../../math"; +import { pointFrom, pointRotateRads } from "../../../math"; interface MultiPositionProps { property: "x" | "y"; @@ -44,8 +44,8 @@ const moveElements = ( origElement.y + origElement.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(origElement.x, origElement.y), - point(cx, cy), + pointFrom(origElement.x, origElement.y), + pointFrom(cx, cy), origElement.angle, ); @@ -97,8 +97,8 @@ const moveGroupTo = ( ]; const [topLeftX, topLeftY] = pointRotateRads( - point(latestElement.x, latestElement.y), - point(cx, cy), + pointFrom(latestElement.x, latestElement.y), + pointFrom(cx, cy), latestElement.angle, ); @@ -171,8 +171,8 @@ const handlePositionChange: DragInputCallbackType< origElement.y + origElement.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(origElement.x, origElement.y), - point(cx, cy), + pointFrom(origElement.x, origElement.y), + pointFrom(cx, cy), origElement.angle, ); @@ -241,8 +241,8 @@ const MultiPosition = ({ const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2]; const [topLeftX, topLeftY] = pointRotateRads( - point(el.x, el.y), - point(cx, cy), + pointFrom(el.x, el.y), + pointFrom(cx, cy), el.angle, ); diff --git a/packages/excalidraw/components/Stats/Position.tsx b/packages/excalidraw/components/Stats/Position.tsx index 8e7671685a..47cc3fe250 100644 --- a/packages/excalidraw/components/Stats/Position.tsx +++ b/packages/excalidraw/components/Stats/Position.tsx @@ -4,7 +4,7 @@ import type { DragInputCallbackType } from "./DragInput"; import { getStepSizedValue, moveElement } from "./utils"; import type Scene from "../../scene/Scene"; import type { AppState } from "../../types"; -import { point, pointRotateRads } from "../../../math"; +import { pointFrom, pointRotateRads } from "../../../math"; interface PositionProps { property: "x" | "y"; @@ -33,8 +33,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ origElement.y + origElement.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(origElement.x, origElement.y), - point(cx, cy), + pointFrom(origElement.x, origElement.y), + pointFrom(cx, cy), origElement.angle, ); @@ -93,8 +93,8 @@ const Position = ({ appState, }: PositionProps) => { const [topLeftX, topLeftY] = pointRotateRads( - point(element.x, element.y), - point(element.x + element.width / 2, element.y + element.height / 2), + pointFrom(element.x, element.y), + pointFrom(element.x + element.width / 2, element.y + element.height / 2), element.angle, ); const value = diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index f281931c87..fc981ce6c6 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -25,7 +25,7 @@ import { API } from "../../tests/helpers/api"; import { actionGroup } from "../../actions"; import { isInGroup } from "../../groups"; import type { Degrees } from "../../../math"; -import { degreesToRadians, point, pointRotateRads } from "../../../math"; +import { degreesToRadians, pointFrom, pointRotateRads } from "../../../math"; const { h } = window; const mouse = new Pointer("mouse"); @@ -264,8 +264,8 @@ describe("stats for a generic element", () => { rectangle.y + rectangle.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); @@ -283,8 +283,8 @@ describe("stats for a generic element", () => { testInputProperty(rectangle, "angle", "A", 0, 45); let [newTopLeftX, newTopLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); @@ -294,8 +294,8 @@ describe("stats for a generic element", () => { testInputProperty(rectangle, "angle", "A", 45, 66); [newTopLeftX, newTopLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); expect(newTopLeftX.toString()).not.toEqual(xInput.value); @@ -311,8 +311,8 @@ describe("stats for a generic element", () => { rectangle.y + rectangle.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); testInputProperty(rectangle, "width", "W", rectangle.width, 400); @@ -321,8 +321,8 @@ describe("stats for a generic element", () => { rectangle.y + rectangle.height / 2, ]; let [currentTopLeftX, currentTopLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); expect(currentTopLeftX).toBeCloseTo(topLeftX, 4); @@ -334,8 +334,8 @@ describe("stats for a generic element", () => { rectangle.y + rectangle.height / 2, ]; [currentTopLeftX, currentTopLeftY] = pointRotateRads( - point(rectangle.x, rectangle.y), - point(cx, cy), + pointFrom(rectangle.x, rectangle.y), + pointFrom(cx, cy), rectangle.angle, ); diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index f6cf167080..a6a443b9b6 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -1,5 +1,5 @@ import type { Radians } from "../../../math"; -import { point, pointRotateRads } from "../../../math"; +import { pointFrom, pointRotateRads } from "../../../math"; import { bindOrUnbindLinearElements, updateBoundElements, @@ -231,8 +231,8 @@ export const moveElement = ( originalElement.y + originalElement.height / 2, ]; const [topLeftX, topLeftY] = pointRotateRads( - point(originalElement.x, originalElement.y), - point(cx, cy), + pointFrom(originalElement.x, originalElement.y), + pointFrom(cx, cy), originalElement.angle, ); @@ -240,8 +240,8 @@ export const moveElement = ( const changeInY = newTopLeftY - topLeftY; const [x, y] = pointRotateRads( - point(newTopLeftX, newTopLeftY), - point(cx + changeInX, cy + changeInY), + pointFrom(newTopLeftX, newTopLeftY), + pointFrom(cx + changeInX, cy + changeInY), -originalElement.angle as Radians, ); diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx index cb11c46cac..9e642fa44f 100644 --- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx +++ b/packages/excalidraw/components/hyperlink/Hyperlink.tsx @@ -36,7 +36,7 @@ import { trackEvent } from "../../analytics"; import { useAppProps, useExcalidrawAppState } from "../App"; import { isEmbeddableElement } from "../../element/typeChecks"; import { getLinkHandleFromCoords } from "./helpers"; -import { point, type GlobalPoint } from "../../../math"; +import { pointFrom, type GlobalPoint } from "../../../math"; const CONTAINER_WIDTH = 320; const SPACE_BOTTOM = 85; @@ -181,7 +181,7 @@ export const Hyperlink = ({ element, elementsMap, appState, - point(event.clientX, event.clientY), + pointFrom(event.clientX, event.clientY), ) as boolean; if (shouldHide) { timeoutId = window.setTimeout(() => { diff --git a/packages/excalidraw/components/hyperlink/helpers.ts b/packages/excalidraw/components/hyperlink/helpers.ts index 3f451a2305..bc470422c9 100644 --- a/packages/excalidraw/components/hyperlink/helpers.ts +++ b/packages/excalidraw/components/hyperlink/helpers.ts @@ -1,5 +1,5 @@ import type { GlobalPoint, Radians } from "../../../math"; -import { point, pointRotateRads } from "../../../math"; +import { pointFrom, pointRotateRads } from "../../../math"; import { MIME_TYPES } from "../../constants"; import type { Bounds } from "../../element/bounds"; import { getElementAbsoluteCoords } from "../../element/bounds"; @@ -35,8 +35,8 @@ export const getLinkHandleFromCoords = ( const y = y1 - dashedLineMargin - linkMarginY + centeringOffset; const [rotatedX, rotatedY] = pointRotateRads( - point(x + linkWidth / 2, y + linkHeight / 2), - point(centerX, centerY), + pointFrom(x + linkWidth / 2, y + linkHeight / 2), + pointFrom(centerX, centerY), angle, ); return [ @@ -85,5 +85,10 @@ export const isPointHittingLink = ( ) { return true; } - return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y)); + return isPointHittingLinkIcon( + element, + elementsMap, + appState, + pointFrom(x, y), + ); }; diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index b476995da1..a8d966e585 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -57,7 +57,7 @@ import { getNormalizedZoom, } from "../scene"; import type { LocalPoint, Radians } from "../../math"; -import { isFiniteNumber, point } from "../../math"; +import { isFiniteNumber, pointFrom } from "../../math"; type RestoredAppState = Omit< AppState, @@ -268,7 +268,7 @@ const restoreElement = ( let y = element.y; let points = // migrate old arrow model to new one !Array.isArray(element.points) || element.points.length < 2 - ? [point(0, 0), point(element.width, element.height)] + ? [pointFrom(0, 0), pointFrom(element.width, element.height)] : element.points; if (points[0][0] !== 0 || points[0][1] !== 0) { @@ -296,7 +296,7 @@ const restoreElement = ( let y: number | undefined = element.y; let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one !Array.isArray(element.points) || element.points.length < 2 - ? [point(0, 0), point(element.width, element.height)] + ? [pointFrom(0, 0), pointFrom(element.width, element.height)] : element.points; if (points[0][0] !== 0 || points[0][1] !== 0) { diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts index d752f6766a..3fecf957b4 100644 --- a/packages/excalidraw/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -2,7 +2,7 @@ import { vi } from "vitest"; import type { ExcalidrawElementSkeleton } from "./transform"; import { convertToExcalidrawElements } from "./transform"; import type { ExcalidrawArrowElement } from "../element/types"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const opts = { regenerateIds: false }; @@ -917,7 +917,7 @@ describe("Test Transform", () => { x: 111.262, y: 57, strokeWidth: 2, - points: [point(0, 0), point(272.985, 0)], + points: [pointFrom(0, 0), pointFrom(272.985, 0)], label: { text: "How are you?", fontSize: 20, @@ -940,7 +940,7 @@ describe("Test Transform", () => { x: 77.017, y: 79, strokeWidth: 2, - points: [point(0, 0)], + points: [pointFrom(0, 0)], label: { text: "Friendship", fontSize: 20, diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index aa276668bf..d1fab0db9a 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -54,7 +54,7 @@ import { randomId } from "../random"; import { syncInvalidIndices } from "../fractionalIndex"; import { getLineHeight } from "../fonts"; import { isArrowElement } from "../element/typeChecks"; -import { point, type LocalPoint } from "../../math"; +import { pointFrom, type LocalPoint } from "../../math"; export type ValidLinearElement = { type: "arrow" | "line"; @@ -537,7 +537,7 @@ export const convertToExcalidrawElements = ( excalidrawElement = newLinearElement({ width, height, - points: [point(0, 0), point(width, height)], + points: [pointFrom(0, 0), pointFrom(width, height)], ...element, }); @@ -550,7 +550,7 @@ export const convertToExcalidrawElements = ( width, height, endArrowhead: "arrow", - points: [point(0, 0), point(width, height)], + points: [pointFrom(0, 0), pointFrom(width, height)], ...element, type: "arrow", }); diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 62e66f645b..d0faa4269d 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -66,7 +66,7 @@ import { import type { LocalPoint, Radians } from "../../math"; import { lineSegment, - point, + pointFrom, pointRotateRads, type GlobalPoint, vectorFromPoint, @@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = ( return vectorToHeading( vectorFromPoint( p, - point( + pointFrom( bindableElement.x + bindableElement.width / 2, bindableElement.y + bindableElement.height / 2, ), @@ -766,15 +766,15 @@ export const bindPointToSnapToElementOutline = ( const intersections = [ ...(intersectElementWithLine( bindableElement, - point(p[0], p[1] - 2 * bindableElement.height), - point(p[0], p[1] + 2 * bindableElement.height), + pointFrom(p[0], p[1] - 2 * bindableElement.height), + pointFrom(p[0], p[1] + 2 * bindableElement.height), FIXED_BINDING_DISTANCE, elementsMap, ) ?? []), ...(intersectElementWithLine( bindableElement, - point(p[0] - 2 * bindableElement.width, p[1]), - point(p[0] + 2 * bindableElement.width, p[1]), + pointFrom(p[0] - 2 * bindableElement.width, p[1]), + pointFrom(p[0] + 2 * bindableElement.width, p[1]), FIXED_BINDING_DISTANCE, elementsMap, ) ?? []), @@ -815,25 +815,25 @@ const headingToMidBindPoint = ( switch (true) { case compareHeading(heading, HEADING_UP): return pointRotateRads( - point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]), + pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]), center, bindableElement.angle, ); case compareHeading(heading, HEADING_RIGHT): return pointRotateRads( - point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1), + pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1), center, bindableElement.angle, ); case compareHeading(heading, HEADING_DOWN): return pointRotateRads( - point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]), + pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]), center, bindableElement.angle, ); default: return pointRotateRads( - point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1), + pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1), center, bindableElement.angle, ); @@ -844,7 +844,7 @@ export const avoidRectangularCorner = ( element: ExcalidrawBindableElement, p: GlobalPoint, ): GlobalPoint => { - const center = point( + const center = pointFrom( element.x + element.width / 2, element.y + element.height / 2, ); @@ -854,13 +854,13 @@ export const avoidRectangularCorner = ( // Top left if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) { return pointRotateRads( - point(element.x - FIXED_BINDING_DISTANCE, element.y), + pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y), center, element.angle, ); } return pointRotateRads( - point(element.x, element.y - FIXED_BINDING_DISTANCE), + pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE), center, element.angle, ); @@ -871,13 +871,16 @@ export const avoidRectangularCorner = ( // Bottom left if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) { return pointRotateRads( - point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE), + pointFrom( + element.x, + element.y + element.height + FIXED_BINDING_DISTANCE, + ), center, element.angle, ); } return pointRotateRads( - point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height), + pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height), center, element.angle, ); @@ -891,7 +894,7 @@ export const avoidRectangularCorner = ( element.width + FIXED_BINDING_DISTANCE ) { return pointRotateRads( - point( + pointFrom( element.x + element.width, element.y + element.height + FIXED_BINDING_DISTANCE, ), @@ -900,7 +903,7 @@ export const avoidRectangularCorner = ( ); } return pointRotateRads( - point( + pointFrom( element.x + element.width + FIXED_BINDING_DISTANCE, element.y + element.height, ), @@ -917,13 +920,16 @@ export const avoidRectangularCorner = ( element.width + FIXED_BINDING_DISTANCE ) { return pointRotateRads( - point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE), + pointFrom( + element.x + element.width, + element.y - FIXED_BINDING_DISTANCE, + ), center, element.angle, ); } return pointRotateRads( - point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y), + pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y), center, element.angle, ); @@ -938,7 +944,10 @@ export const snapToMid = ( tolerance: number = 0.05, ): GlobalPoint => { const { x, y, width, height, angle } = element; - const center = point(x + width / 2 - 0.1, y + height / 2 - 0.1); + const center = pointFrom( + x + width / 2 - 0.1, + y + height / 2 - 0.1, + ); const nonRotated = pointRotateRads(p, center, -angle as Radians); // snap-to-center point is adaptive to element size, but we don't want to go @@ -953,7 +962,7 @@ export const snapToMid = ( ) { // LEFT return pointRotateRads( - point(x - FIXED_BINDING_DISTANCE, center[1]), + pointFrom(x - FIXED_BINDING_DISTANCE, center[1]), center, angle, ); @@ -964,7 +973,7 @@ export const snapToMid = ( ) { // TOP return pointRotateRads( - point(center[0], y - FIXED_BINDING_DISTANCE), + pointFrom(center[0], y - FIXED_BINDING_DISTANCE), center, angle, ); @@ -975,7 +984,7 @@ export const snapToMid = ( ) { // RIGHT return pointRotateRads( - point(x + width + FIXED_BINDING_DISTANCE, center[1]), + pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]), center, angle, ); @@ -986,7 +995,7 @@ export const snapToMid = ( ) { // DOWN return pointRotateRads( - point(center[0], y + height + FIXED_BINDING_DISTANCE), + pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE), center, angle, ); @@ -1023,11 +1032,11 @@ const updateBoundPoint = ( startOrEnd === "startBinding" ? "start" : "end", elementsMap, ).fixedPoint; - const globalMidPoint = point( + const globalMidPoint = pointFrom( bindableElement.x + bindableElement.width / 2, bindableElement.y + bindableElement.height / 2, ); - const global = point( + const global = pointFrom( bindableElement.x + fixedPoint[0] * bindableElement.width, bindableElement.y + fixedPoint[1] * bindableElement.height, ); @@ -1118,7 +1127,7 @@ export const calculateFixedPointForElbowArrowBinding = ( hoveredElement, elementsMap, ); - const globalMidPoint = point( + const globalMidPoint = pointFrom( bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2, ); @@ -1337,9 +1346,9 @@ export const bindingBorderTest = ( const threshold = maxBindingGap(element, element.width, element.height); const shape = getElementShape(element, elementsMap); return ( - isPointOnShape(point(x, y), shape, threshold) || + isPointOnShape(pointFrom(x, y), shape, threshold) || (fullShape === true && - pointInsideBounds(point(x, y), aabbForElement(element))) + pointInsideBounds(pointFrom(x, y), aabbForElement(element))) ); }; @@ -2197,11 +2206,11 @@ export const getGlobalFixedPointForBindableElement = ( const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio); return pointRotateRads( - point( + pointFrom( element.x + element.width * fixedX, element.y + element.height * fixedY, ), - point( + pointFrom( element.x + element.width / 2, element.y + element.height / 2, ), @@ -2229,7 +2238,7 @@ const getGlobalFixedPoints = ( arrow.startBinding.fixedPoint, startElement as ExcalidrawBindableElement, ) - : point( + : pointFrom( arrow.x + arrow.points[0][0], arrow.y + arrow.points[0][1], ); @@ -2239,7 +2248,7 @@ const getGlobalFixedPoints = ( arrow.endBinding.fixedPoint, endElement as ExcalidrawBindableElement, ) - : point( + : pointFrom( arrow.x + arrow.points[arrow.points.length - 1][0], arrow.y + arrow.points[arrow.points.length - 1][1], ); diff --git a/packages/excalidraw/element/bounds.test.ts b/packages/excalidraw/element/bounds.test.ts index f5ca0e901b..ffa89bb3e8 100644 --- a/packages/excalidraw/element/bounds.test.ts +++ b/packages/excalidraw/element/bounds.test.ts @@ -1,5 +1,5 @@ import type { LocalPoint } from "../../math"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; import { ROUNDNESS } from "../constants"; import { arrayToMap } from "../utils"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; @@ -125,9 +125,9 @@ describe("getElementBounds", () => { a: 0.6447741904932416, }), points: [ - point(0, 0), - point(67.33984375, 92.48828125), - point(-102.7890625, 52.15625), + pointFrom(0, 0), + pointFrom(67.33984375, 92.48828125), + pointFrom(-102.7890625, 52.15625), ], } as ExcalidrawLinearElement; diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 16f431855c..6fedd4113e 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -34,7 +34,7 @@ import type { import { degreesToRadians, lineSegment, - point, + pointFrom, pointDistance, pointFromArray, pointRotateRads, @@ -113,8 +113,8 @@ export class ElementBounds { const [minX, minY, maxX, maxY] = getBoundsFromPoints( element.points.map(([x, y]) => pointRotateRads( - point(x, y), - point(cx - element.x, cy - element.y), + pointFrom(x, y), + pointFrom(cx - element.x, cy - element.y), element.angle, ), ), @@ -130,23 +130,23 @@ export class ElementBounds { bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); } else if (element.type === "diamond") { const [x11, y11] = pointRotateRads( - point(cx, y1), - point(cx, cy), + pointFrom(cx, y1), + pointFrom(cx, cy), element.angle, ); const [x12, y12] = pointRotateRads( - point(cx, y2), - point(cx, cy), + pointFrom(cx, y2), + pointFrom(cx, cy), element.angle, ); const [x22, y22] = pointRotateRads( - point(x1, cy), - point(cx, cy), + pointFrom(x1, cy), + pointFrom(cx, cy), element.angle, ); const [x21, y21] = pointRotateRads( - point(x2, cy), - point(cx, cy), + pointFrom(x2, cy), + pointFrom(cx, cy), element.angle, ); const minX = Math.min(x11, x12, x22, x21); @@ -164,23 +164,23 @@ export class ElementBounds { bounds = [cx - ww, cy - hh, cx + ww, cy + hh]; } else { const [x11, y11] = pointRotateRads( - point(x1, y1), - point(cx, cy), + pointFrom(x1, y1), + pointFrom(cx, cy), element.angle, ); const [x12, y12] = pointRotateRads( - point(x1, y2), - point(cx, cy), + pointFrom(x1, y2), + pointFrom(cx, cy), element.angle, ); const [x22, y22] = pointRotateRads( - point(x2, y2), - point(cx, cy), + pointFrom(x2, y2), + pointFrom(cx, cy), element.angle, ); const [x21, y21] = pointRotateRads( - point(x2, y1), - point(cx, cy), + pointFrom(x2, y1), + pointFrom(cx, cy), element.angle, ); const minX = Math.min(x11, x12, x22, x21); @@ -255,7 +255,7 @@ export const getElementLineSegments = ( elementsMap, ); - const center: GlobalPoint = point(cx, cy); + const center: GlobalPoint = pointFrom(cx, cy); if (isLinearElement(element) || isFreeDrawElement(element)) { const segments: LineSegment[] = []; @@ -266,7 +266,7 @@ export const getElementLineSegments = ( segments.push( lineSegment( pointRotateRads( - point( + pointFrom( element.points[i][0] + element.x, element.points[i][1] + element.y, ), @@ -274,7 +274,7 @@ export const getElementLineSegments = ( element.angle, ), pointRotateRads( - point( + pointFrom( element.points[i + 1][0] + element.x, element.points[i + 1][1] + element.y, ), @@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = ( ops: Op[], transformXY?: (p: GlobalPoint) => GlobalPoint, ): Bounds => { - let currentP: GlobalPoint = point(0, 0); + let currentP: GlobalPoint = pointFrom(0, 0); const { minX, minY, maxX, maxY } = ops.reduce( (limits, { op, data }) => { @@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = ( // move operation does not draw anything; so, it always // returns false } else if (op === "bcurveTo") { - const _p1 = point(data[0], data[1]); - const _p2 = point(data[2], data[3]); - const _p3 = point(data[4], data[5]); + const _p1 = pointFrom(data[0], data[1]); + const _p2 = pointFrom(data[2], data[3]); + const _p3 = pointFrom(data[4], data[5]); const p1 = transformXY ? transformXY(_p1) : _p1; const p2 = transformXY ? transformXY(_p2) : _p2; @@ -591,21 +591,21 @@ export const getArrowheadPoints = ( invariant(data.length === 6, "Op data length is not 6"); - const p3 = point(data[4], data[5]); - const p2 = point(data[2], data[3]); - const p1 = point(data[0], data[1]); + const p3 = pointFrom(data[4], data[5]); + const p2 = pointFrom(data[2], data[3]); + const p1 = pointFrom(data[0], data[1]); // We need to find p0 of the bezier curve. // It is typically the last point of the previous // curve; it can also be the position of moveTo operation. const prevOp = ops[index - 1]; - let p0 = point(0, 0); + let p0 = pointFrom(0, 0); if (prevOp.op === "move") { const p = pointFromArray(prevOp.data); invariant(p != null, "Op data is not a point"); p0 = p; } else if (prevOp.op === "bcurveTo") { - p0 = point(prevOp.data[4], prevOp.data[5]); + p0 = pointFrom(prevOp.data[4], prevOp.data[5]); } // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 @@ -671,13 +671,13 @@ export const getArrowheadPoints = ( // Return points const [x3, y3] = pointRotateRads( - point(xs, ys), - point(x2, y2), + pointFrom(xs, ys), + pointFrom(x2, y2), ((-angle * Math.PI) / 180) as Radians, ); const [x4, y4] = pointRotateRads( - point(xs, ys), - point(x2, y2), + pointFrom(xs, ys), + pointFrom(x2, y2), degreesToRadians(angle), ); @@ -690,8 +690,8 @@ export const getArrowheadPoints = ( const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0]; [ox, oy] = pointRotateRads( - point(x2 + minSize * 2, y2), - point(x2, y2), + pointFrom(x2 + minSize * 2, y2), + pointFrom(x2, y2), Math.atan2(py - y2, px - x2) as Radians, ); } else { @@ -701,8 +701,8 @@ export const getArrowheadPoints = ( : [0, 0]; [ox, oy] = pointRotateRads( - point(x2 - minSize * 2, y2), - point(x2, y2), + pointFrom(x2 - minSize * 2, y2), + pointFrom(x2, y2), Math.atan2(y2 - py, x2 - px) as Radians, ); } @@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = ( if (element.points.length < 2) { const [pointX, pointY] = element.points[0]; const [x, y] = pointRotateRads( - point(element.x + pointX, element.y + pointY), - point(cx, cy), + pointFrom(element.x + pointX, element.y + pointY), + pointFrom(cx, cy), element.angle, ); @@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = ( const ops = getCurvePathOps(shape); const transformXY = ([x, y]: GlobalPoint) => pointRotateRads( - point(element.x + x, element.y + y), - point(cx, cy), + pointFrom(element.x + x, element.y + y), + pointFrom(cx, cy), element.angle, ); const res = getMinMaxXYFromCurvePathOps(ops, transformXY); @@ -931,8 +931,8 @@ export const getClosestElementBounds = ( elements.forEach((element) => { const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); const distance = pointDistance( - point((x1 + x2) / 2, (y1 + y2) / 2), - point(from.x, from.y), + pointFrom((x1 + x2) / 2, (y1 + y2) / 2), + pointFrom(from.x, from.y), ); if (distance < minDistance) { @@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({ }; export const getCenterForBounds = (bounds: Bounds): GlobalPoint => - point( + pointFrom( bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2, ); diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 7eafa7dfa0..a1593d2f69 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -17,7 +17,7 @@ import { } from "./typeChecks"; import { getBoundTextShape, isPathALoop } from "../shapes"; import type { GlobalPoint, LocalPoint, Polygon } from "../../math"; -import { isPointWithinBounds, point } from "../../math"; +import { isPointWithinBounds, pointFrom } from "../../math"; export const shouldTestInside = (element: ExcalidrawElement) => { if (element.type === "arrow") { @@ -61,13 +61,13 @@ export const hitElementItself = ({ let hit = shouldTestInside(element) ? // Since `inShape` tests STRICTLY againt the insides of a shape // we would need `onShape` as well to include the "borders" - isPointInShape(point(x, y), shape) || - isPointOnShape(point(x, y), shape, threshold) - : isPointOnShape(point(x, y), shape, threshold); + isPointInShape(pointFrom(x, y), shape) || + isPointOnShape(pointFrom(x, y), shape, threshold) + : isPointOnShape(pointFrom(x, y), shape, threshold); // hit test against a frame's name if (!hit && frameNameBound) { - hit = isPointInShape(point(x, y), { + hit = isPointInShape(pointFrom(x, y), { type: "polygon", data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) .data as Polygon, @@ -89,7 +89,11 @@ export const hitElementBoundingBox = ( y1 -= tolerance; x2 += tolerance; y2 += tolerance; - return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2)); + return isPointWithinBounds( + pointFrom(x1, y1), + pointFrom(x, y), + pointFrom(x2, y2), + ); }; export const hitElementBoundingBoxOnly = < @@ -115,5 +119,5 @@ export const hitElementBoundText = ( y: number, textShape: GeometricShape | null, ): boolean => { - return !!textShape && isPointInShape(point(x, y), textShape); + return !!textShape && isPointInShape(pointFrom(x, y), textShape); }; diff --git a/packages/excalidraw/element/flowchart.ts b/packages/excalidraw/element/flowchart.ts index cc174bfa93..8c14bc01a7 100644 --- a/packages/excalidraw/element/flowchart.ts +++ b/packages/excalidraw/element/flowchart.ts @@ -29,7 +29,7 @@ import { isFlowchartNodeElement, } from "./typeChecks"; import { invariant } from "../utils"; -import { point, type LocalPoint } from "../../math"; +import { pointFrom, type LocalPoint } from "../../math"; import { aabbForElement } from "../shapes"; type LinkDirection = "up" | "right" | "down" | "left"; @@ -421,7 +421,7 @@ const createBindingArrow = ( strokeColor: appState.currentItemStrokeColor, strokeStyle: appState.currentItemStrokeStyle, strokeWidth: appState.currentItemStrokeWidth, - points: [point(0, 0), point(endX, endY)], + points: [pointFrom(0, 0), pointFrom(endX, endY)], elbowed: true, }); diff --git a/packages/excalidraw/element/heading.ts b/packages/excalidraw/element/heading.ts index b22316c6a2..c17a077fcb 100644 --- a/packages/excalidraw/element/heading.ts +++ b/packages/excalidraw/element/heading.ts @@ -6,7 +6,7 @@ import type { Radians, } from "../../math"; import { - point, + pointFrom, pointRotateRads, pointScaleFromOrigin, radiansToDegrees, @@ -82,7 +82,7 @@ export const headingForPointFromElement = < const top = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width / 2, element.y), + pointFrom(element.x + element.width / 2, element.y), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -91,7 +91,7 @@ export const headingForPointFromElement = < ); const right = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width, element.y + element.height / 2), + pointFrom(element.x + element.width, element.y + element.height / 2), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -100,7 +100,7 @@ export const headingForPointFromElement = < ); const bottom = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width / 2, element.y + element.height), + pointFrom(element.x + element.width / 2, element.y + element.height), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -109,7 +109,7 @@ export const headingForPointFromElement = < ); const left = pointRotateRads( pointScaleFromOrigin( - point(element.x, element.y + element.height / 2), + pointFrom(element.x, element.y + element.height / 2), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -133,22 +133,22 @@ export const headingForPointFromElement = < } const topLeft = pointScaleFromOrigin( - point(aabb[0], aabb[1]), + pointFrom(aabb[0], aabb[1]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const topRight = pointScaleFromOrigin( - point(aabb[2], aabb[1]), + pointFrom(aabb[2], aabb[1]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const bottomLeft = pointScaleFromOrigin( - point(aabb[0], aabb[3]), + pointFrom(aabb[0], aabb[3]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const bottomRight = pointScaleFromOrigin( - point(aabb[2], aabb[3]), + pointFrom(aabb[2], aabb[3]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index e11c0b158c..0d1db77331 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -49,7 +49,7 @@ import type Scene from "../scene/Scene"; import type { Radians } from "../../math"; import { pointCenter, - point, + pointFrom, pointRotateRads, pointsEqual, vector, @@ -108,7 +108,7 @@ export class LinearElementEditor { this.elementId = element.id as string & { _brand: "excalidrawLinearElementId"; }; - if (!pointsEqual(element.points[0], point(0, 0))) { + if (!pointsEqual(element.points[0], pointFrom(0, 0))) { console.error("Linear element is not normalized", Error().stack); } @@ -287,7 +287,7 @@ export class LinearElementEditor { element, elementsMap, referencePoint, - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -296,7 +296,7 @@ export class LinearElementEditor { [ { index: selectedIndex, - point: point( + point: pointFrom( width + referencePoint[0], height + referencePoint[1], ), @@ -329,7 +329,7 @@ export class LinearElementEditor { scenePointerY - linearElementEditor.pointerOffset.y, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ) - : point( + : pointFrom( element.points[pointIndex][0] + deltaX, element.points[pointIndex][1] + deltaY, ); @@ -590,11 +590,11 @@ export class LinearElementEditor { linearElementEditor.segmentMidPointHoveredCoords; if (existingSegmentMidpointHitCoords) { const distance = pointDistance( - point( + pointFrom( existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[1], ), - point(scenePointer.x, scenePointer.y), + pointFrom(scenePointer.x, scenePointer.y), ); if (distance <= threshold) { return existingSegmentMidpointHitCoords; @@ -606,8 +606,8 @@ export class LinearElementEditor { while (index < midPoints.length) { if (midPoints[index] !== null) { const distance = pointDistance( - point(midPoints[index]![0], midPoints[index]![1]), - point(scenePointer.x, scenePointer.y), + pointFrom(midPoints[index]![0], midPoints[index]![1]), + pointFrom(scenePointer.x, scenePointer.y), ); if (distance <= threshold) { return midPoints[index]; @@ -626,8 +626,8 @@ export class LinearElementEditor { zoom: AppState["zoom"], ) { let distance = pointDistance( - point(startPoint[0], startPoint[1]), - point(endPoint[0], endPoint[1]), + pointFrom(startPoint[0], startPoint[1]), + pointFrom(endPoint[0], endPoint[1]), ); if (element.points.length > 2 && element.roundness) { distance = getBezierCurveLength(element, endPoint); @@ -829,11 +829,11 @@ export class LinearElementEditor { const targetPoint = clickedPointIndex > -1 && pointRotateRads( - point( + pointFrom( element.x + element.points[clickedPointIndex][0], element.y + element.points[clickedPointIndex][1], ), - point(cx, cy), + pointFrom(cx, cy), element.angle, ); @@ -928,11 +928,11 @@ export class LinearElementEditor { element, elementsMap, lastCommittedPoint, - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); - newPoint = point( + newPoint = pointFrom( width + lastCommittedPoint[0], height + lastCommittedPoint[1], ); @@ -984,8 +984,8 @@ export class LinearElementEditor { const { x, y } = element; return pointRotateRads( - point(x + p[0], y + p[1]), - point(cx, cy), + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), element.angle, ); } @@ -1001,8 +1001,8 @@ export class LinearElementEditor { return element.points.map((p) => { const { x, y } = element; return pointRotateRads( - point(x + p[0], y + p[1]), - point(cx, cy), + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), element.angle, ); }); @@ -1025,8 +1025,12 @@ export class LinearElementEditor { const { x, y } = element; return p - ? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle) - : pointRotateRads(point(x, y), point(cx, cy), element.angle); + ? pointRotateRads( + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), + element.angle, + ) + : pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle); } static pointFromAbsoluteCoords( @@ -1036,7 +1040,7 @@ export class LinearElementEditor { ): LocalPoint { if (isElbowArrow(element)) { // No rotation for elbow arrows - return point( + return pointFrom( absoluteCoords[0] - element.x, absoluteCoords[1] - element.y, ); @@ -1046,11 +1050,11 @@ export class LinearElementEditor { const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [x, y] = pointRotateRads( - point(absoluteCoords[0], absoluteCoords[1]), - point(cx, cy), + pointFrom(absoluteCoords[0], absoluteCoords[1]), + pointFrom(cx, cy), -element.angle as Radians, ); - return point(x - element.x, y - element.y); + return pointFrom(x - element.x, y - element.y); } static getPointIndexUnderCursor( @@ -1071,7 +1075,7 @@ export class LinearElementEditor { while (--idx > -1) { const p = pointHandles[idx]; if ( - pointDistance(point(x, y), point(p[0], p[1])) * zoom.value < + pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value < // +1px to account for outline stroke LinearElementEditor.POINT_HANDLE_SIZE + 1 ) { @@ -1093,12 +1097,12 @@ export class LinearElementEditor { const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [rotatedX, rotatedY] = pointRotateRads( - point(pointerOnGrid[0], pointerOnGrid[1]), - point(cx, cy), + pointFrom(pointerOnGrid[0], pointerOnGrid[1]), + pointFrom(cx, cy), -element.angle as Radians, ); - return point(rotatedX - element.x, rotatedY - element.y); + return pointFrom(rotatedX - element.x, rotatedY - element.y); } /** @@ -1118,7 +1122,7 @@ export class LinearElementEditor { return { points: points.map((p) => { - return point(p[0] - offsetX, p[1] - offsetY); + return pointFrom(p[0] - offsetX, p[1] - offsetY); }), x: element.x + offsetX, y: element.y + offsetY, @@ -1172,8 +1176,8 @@ export class LinearElementEditor { } acc.push( nextPoint - ? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) - : point(p[0], p[1]), + ? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) + : pointFrom(p[0], p[1]), ); nextSelectedIndices.push(indexCursor + 1); @@ -1194,7 +1198,7 @@ export class LinearElementEditor { [ { index: element.points.length - 1, - point: point(lastPoint[0] + 30, lastPoint[1] + 30), + point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30), }, ], elementsMap, @@ -1235,7 +1239,9 @@ export class LinearElementEditor { const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => { if (!pointIndices.includes(idx)) { acc.push( - !acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY), + !acc.length + ? pointFrom(0, 0) + : pointFrom(p[0] - offsetX, p[1] - offsetY), ); } return acc; @@ -1312,9 +1318,9 @@ export class LinearElementEditor { const deltaY = selectedPointData.point[1] - points[selectedPointData.index][1]; - return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); + return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); } - return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p; + return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p; }); LinearElementEditor._updatePoints( @@ -1368,8 +1374,8 @@ export class LinearElementEditor { const origin = linearElementEditor.pointerDownState.origin!; const dist = pointDistance( - point(origin.x, origin.y), - point(pointerCoords.x, pointerCoords.y), + pointFrom(origin.x, origin.y), + pointFrom(pointerCoords.x, pointerCoords.y), ); if ( !appState.editingLinearElement && @@ -1493,8 +1499,8 @@ export class LinearElementEditor { const dX = prevCenterX - nextCenterX; const dY = prevCenterY - nextCenterY; const rotated = pointRotateRads( - point(offsetX, offsetY), - point(dX, dY), + pointFrom(offsetX, offsetY), + pointFrom(dX, dY), element.angle, ); mutateElement(element, { @@ -1540,8 +1546,8 @@ export class LinearElementEditor { ); return pointRotateRads( - point(width, height), - point(0, 0), + pointFrom(width, height), + pointFrom(0, 0), -element.angle as Radians, ); } @@ -1611,36 +1617,36 @@ export class LinearElementEditor { ); const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextY2 = boundTextY1 + boundTextElement.height; - const centerPoint = point(cx, cy); + const centerPoint = pointFrom(cx, cy); const topLeftRotatedPoint = pointRotateRads( - point(x1, y1), + pointFrom(x1, y1), centerPoint, element.angle, ); const topRightRotatedPoint = pointRotateRads( - point(x2, y1), + pointFrom(x2, y1), centerPoint, element.angle, ); const counterRotateBoundTextTopLeft = pointRotateRads( - point(boundTextX1, boundTextY1), + pointFrom(boundTextX1, boundTextY1), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextTopRight = pointRotateRads( - point(boundTextX2, boundTextY1), + pointFrom(boundTextX2, boundTextY1), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextBottomLeft = pointRotateRads( - point(boundTextX1, boundTextY2), + pointFrom(boundTextX1, boundTextY2), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextBottomRight = pointRotateRads( - point(boundTextX2, boundTextY2), + pointFrom(boundTextX2, boundTextY2), centerPoint, -element.angle as Radians, ); diff --git a/packages/excalidraw/element/newElement.test.ts b/packages/excalidraw/element/newElement.test.ts index 770d0d9870..6a74e58f05 100644 --- a/packages/excalidraw/element/newElement.test.ts +++ b/packages/excalidraw/element/newElement.test.ts @@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants"; import { isPrimitive } from "../utils"; import type { ExcalidrawLinearElement } from "./types"; import type { LocalPoint } from "../../math"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const assertCloneObjects = (source: any, clone: any) => { for (const key in clone) { @@ -38,7 +38,7 @@ describe("duplicating single elements", () => { element.__proto__ = { hello: "world" }; mutateElement(element, { - points: [point(1, 2), point(3, 4)], + points: [pointFrom(1, 2), pointFrom(3, 4)], }); const copy = duplicateElement(null, new Map(), element); diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 0a01459e69..08ca5543f8 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -58,7 +58,7 @@ import type { GlobalPoint } from "../../math"; import { pointCenter, normalizeRadians, - point, + pointFrom, pointFromPair, pointRotateRads, type Radians, @@ -240,8 +240,8 @@ const resizeSingleTextElement = ( ); // rotation pointer with reverse angle const [rotatedX, rotatedY] = pointRotateRads( - point(pointerX, pointerY), - point(cx, cy), + pointFrom(pointerX, pointerY), + pointFrom(cx, cy), -element.angle as Radians, ); let scaleX = 0; @@ -276,23 +276,23 @@ const resizeSingleTextElement = ( const startBottomRight = [x2, y2]; const startCenter = [cx, cy]; - let newTopLeft = point(x1, y1); + let newTopLeft = pointFrom(x1, y1); if (["n", "w", "nw"].includes(transformHandleType)) { - newTopLeft = point( + newTopLeft = pointFrom( startBottomRight[0] - Math.abs(nextWidth), startBottomRight[1] - Math.abs(nextHeight), ); } if (transformHandleType === "ne") { const bottomLeft = [startTopLeft[0], startBottomRight[1]]; - newTopLeft = point( + newTopLeft = pointFrom( bottomLeft[0], bottomLeft[1] - Math.abs(nextHeight), ); } if (transformHandleType === "sw") { const topRight = [startBottomRight[0], startTopLeft[1]]; - newTopLeft = point( + newTopLeft = pointFrom( topRight[0] - Math.abs(nextWidth), topRight[1], ); @@ -311,12 +311,20 @@ const resizeSingleTextElement = ( } const angle = element.angle; - const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle); - const newCenter = point( + const rotatedTopLeft = pointRotateRads( + newTopLeft, + pointFrom(cx, cy), + angle, + ); + const newCenter = pointFrom( newTopLeft[0] + Math.abs(nextWidth) / 2, newTopLeft[1] + Math.abs(nextHeight) / 2, ); - const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle); + const rotatedNewCenter = pointRotateRads( + newCenter, + pointFrom(cx, cy), + angle, + ); newTopLeft = pointRotateRads( rotatedTopLeft, rotatedNewCenter, @@ -341,12 +349,12 @@ const resizeSingleTextElement = ( stateAtResizeStart.height, true, ); - const startTopLeft = point(x1, y1); - const startBottomRight = point(x2, y2); + const startTopLeft = pointFrom(x1, y1); + const startBottomRight = pointFrom(x2, y2); const startCenter = pointCenter(startTopLeft, startBottomRight); const rotatedPointer = pointRotateRads( - point(pointerX, pointerY), + pointFrom(pointerX, pointerY), startCenter, -stateAtResizeStart.angle as Radians, ); @@ -419,7 +427,7 @@ const resizeSingleTextElement = ( startCenter, angle, ); - const newCenter = point( + const newCenter = pointFrom( newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2, ); @@ -461,13 +469,13 @@ export const resizeSingleElement = ( stateAtResizeStart.height, true, ); - const startTopLeft = point(x1, y1); - const startBottomRight = point(x2, y2); + const startTopLeft = pointFrom(x1, y1); + const startBottomRight = pointFrom(x2, y2); const startCenter = pointCenter(startTopLeft, startBottomRight); // Calculate new dimensions based on cursor position const rotatedPointer = pointRotateRads( - point(pointerX, pointerY), + pointFrom(pointerX, pointerY), startCenter, -stateAtResizeStart.angle as Radians, ); @@ -648,7 +656,7 @@ export const resizeSingleElement = ( startCenter, angle, ); - const newCenter = point( + const newCenter = pointFrom( newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2, ); @@ -817,20 +825,20 @@ export const resizeMultipleElements = ( const direction = transformHandleType; const anchorsMap: Record = { - ne: point(minX, maxY), - se: point(minX, minY), - sw: point(maxX, minY), - nw: point(maxX, maxY), - e: point(minX, minY + height / 2), - w: point(maxX, minY + height / 2), - n: point(minX + width / 2, maxY), - s: point(minX + width / 2, minY), + ne: pointFrom(minX, maxY), + se: pointFrom(minX, minY), + sw: pointFrom(maxX, minY), + nw: pointFrom(maxX, maxY), + e: pointFrom(minX, minY + height / 2), + w: pointFrom(maxX, minY + height / 2), + n: pointFrom(minX + width / 2, maxY), + s: pointFrom(minX + width / 2, minY), }; // anchor point must be on the opposite side of the dragged selection handle // or be the center of the selection if shouldResizeFromCenter const [anchorX, anchorY] = shouldResizeFromCenter - ? point(midX, midY) + ? pointFrom(midX, midY) : anchorsMap[direction]; const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1; @@ -1044,8 +1052,8 @@ const rotateMultipleElements = ( const origAngle = originalElements.get(element.id)?.angle ?? element.angle; const [rotatedCX, rotatedCY] = pointRotateRads( - point(cx, cy), - point(centerX, centerY), + pointFrom(cx, cy), + pointFrom(centerX, centerY), (centerAngle + origAngle - element.angle) as Radians, ); @@ -1101,40 +1109,44 @@ export const getResizeOffsetXY = ( const angle = ( selectedElements.length === 1 ? selectedElements[0].angle : 0 ) as Radians; - [x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians); + [x, y] = pointRotateRads( + pointFrom(x, y), + pointFrom(cx, cy), + -angle as Radians, + ); switch (transformHandleType) { case "n": return pointRotateRads( - point(x - (x1 + x2) / 2, y - y1), - point(0, 0), + pointFrom(x - (x1 + x2) / 2, y - y1), + pointFrom(0, 0), angle, ); case "s": return pointRotateRads( - point(x - (x1 + x2) / 2, y - y2), - point(0, 0), + pointFrom(x - (x1 + x2) / 2, y - y2), + pointFrom(0, 0), angle, ); case "w": return pointRotateRads( - point(x - x1, y - (y1 + y2) / 2), - point(0, 0), + pointFrom(x - x1, y - (y1 + y2) / 2), + pointFrom(0, 0), angle, ); case "e": return pointRotateRads( - point(x - x2, y - (y1 + y2) / 2), - point(0, 0), + pointFrom(x - x2, y - (y1 + y2) / 2), + pointFrom(0, 0), angle, ); case "nw": - return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle); case "ne": - return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle); case "sw": - return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle); case "se": - return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle); default: return [0, 0]; } diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts index c363f61806..5fcae53359 100644 --- a/packages/excalidraw/element/resizeTest.ts +++ b/packages/excalidraw/element/resizeTest.ts @@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants"; import { isLinearElement } from "./typeChecks"; import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import { - point, + pointFrom, pointOnLineSegment, pointRotateRads, type Radians, @@ -92,16 +92,20 @@ export const resizeTest = ( if (!(isLinearElement(element) && element.points.length <= 2)) { const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const sides = getSelectionBorders( - point(x1 - SPACING, y1 - SPACING), - point(x2 + SPACING, y2 + SPACING), - point(cx, cy), + pointFrom(x1 - SPACING, y1 - SPACING), + pointFrom(x2 + SPACING, y2 + SPACING), + pointFrom(cx, cy), element.angle, ); for (const [dir, side] of Object.entries(sides)) { // test to see if x, y are on the line segment if ( - pointOnLineSegment(point(x, y), side as LineSegment, SPACING) + pointOnLineSegment( + pointFrom(x, y), + side as LineSegment, + SPACING, + ) ) { return dir as TransformHandleType; } @@ -178,9 +182,9 @@ export const getTransformHandleTypeFromCoords = < const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const sides = getSelectionBorders( - point(x1 - SPACING, y1 - SPACING), - point(x2 + SPACING, y2 + SPACING), - point(cx, cy), + pointFrom(x1 - SPACING, y1 - SPACING), + pointFrom(x2 + SPACING, y2 + SPACING), + pointFrom(cx, cy), 0 as Radians, ); @@ -188,7 +192,7 @@ export const getTransformHandleTypeFromCoords = < // test to see if x, y are on the line segment if ( pointOnLineSegment( - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), side as LineSegment, SPACING, ) @@ -265,10 +269,10 @@ const getSelectionBorders = ( center: Point, angle: Radians, ) => { - const topLeft = pointRotateRads(point(x1, y1), center, angle); - const topRight = pointRotateRads(point(x2, y1), center, angle); - const bottomLeft = pointRotateRads(point(x1, y2), center, angle); - const bottomRight = pointRotateRads(point(x2, y2), center, angle); + const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle); + const topRight = pointRotateRads(pointFrom(x2, y1), center, angle); + const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle); + const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle); return { n: [topLeft, topRight], diff --git a/packages/excalidraw/element/routing.test.tsx b/packages/excalidraw/element/routing.test.tsx index e451fae5d2..fb6b23f286 100644 --- a/packages/excalidraw/element/routing.test.tsx +++ b/packages/excalidraw/element/routing.test.tsx @@ -17,7 +17,7 @@ import type { ExcalidrawElbowArrowElement, } from "./types"; import { ARROW_TYPE } from "../constants"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const { h } = window; @@ -32,8 +32,8 @@ describe("elbow arrow routing", () => { }) as ExcalidrawElbowArrowElement; scene.insertElement(arrow); mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [ - point(-45 - arrow.x, -100.1 - arrow.y), - point(45 - arrow.x, 99.9 - arrow.y), + pointFrom(-45 - arrow.x, -100.1 - arrow.y), + pointFrom(45 - arrow.x, 99.9 - arrow.y), ]); expect(arrow.points).toEqual([ [0, 0], @@ -69,7 +69,7 @@ describe("elbow arrow routing", () => { y: -100.1, width: 90, height: 200, - points: [point(0, 0), point(90, 200)], + points: [pointFrom(0, 0), pointFrom(90, 200)], }) as ExcalidrawElbowArrowElement; scene.insertElement(rectangle1); scene.insertElement(rectangle2); @@ -81,7 +81,7 @@ describe("elbow arrow routing", () => { expect(arrow.startBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null); - mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]); + mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]); expect(arrow.points).toEqual([ [0, 0], diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index 895340c91a..c8b1c2d431 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -1,6 +1,6 @@ import type { Radians } from "../../math"; import { - point, + pointFrom, pointScaleFromOrigin, pointTranslate, vector, @@ -743,13 +743,13 @@ const getDonglePosition = ( ): GlobalPoint => { switch (heading) { case HEADING_UP: - return point(p[0], bounds[1]); + return pointFrom(p[0], bounds[1]); case HEADING_RIGHT: - return point(bounds[2], p[1]); + return pointFrom(bounds[2], p[1]); case HEADING_DOWN: - return point(p[0], bounds[3]); + return pointFrom(p[0], bounds[3]); } - return point(bounds[0], p[1]); + return pointFrom(bounds[0], p[1]); }; const estimateSegmentCount = ( diff --git a/packages/excalidraw/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx index 98063f05b1..ea57ca190c 100644 --- a/packages/excalidraw/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -19,7 +19,7 @@ import type { import { API } from "../tests/helpers/api"; import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -42,7 +42,7 @@ describe("textWysiwyg", () => { type: "line", width: 100, height: 0, - points: [point(0, 0), point(100, 0)], + points: [pointFrom(0, 0), pointFrom(100, 0)], }); const textSize = 20; const text = API.createElement({ diff --git a/packages/excalidraw/element/transformHandles.ts b/packages/excalidraw/element/transformHandles.ts index 173c9fdc9c..ccd68b2828 100644 --- a/packages/excalidraw/element/transformHandles.ts +++ b/packages/excalidraw/element/transformHandles.ts @@ -19,7 +19,7 @@ import { isIOS, } from "../constants"; import type { Radians } from "../../math"; -import { point, pointRotateRads } from "../../math"; +import { pointFrom, pointRotateRads } from "../../math"; export type TransformHandleDirection = | "n" @@ -95,8 +95,8 @@ const generateTransformHandle = ( angle: Radians, ): TransformHandle => { const [xx, yy] = pointRotateRads( - point(x + width / 2, y + height / 2), - point(cx, cy), + pointFrom(x + width / 2, y + height / 2), + pointFrom(cx, cy), angle, ); return [xx - width / 2, yy - height / 2, width, height]; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index fb9a45820f..a8e91265fc 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -29,7 +29,7 @@ import { getElementLineSegments } from "./element/bounds"; import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; import type { ReadonlySetLike } from "./utility-types"; -import { isPointWithinBounds, point } from "../math"; +import { isPointWithinBounds, pointFrom } from "../math"; // --------------------------- Frame State ------------------------------------ export const bindElementsToFramesAfterDuplication = ( @@ -159,9 +159,9 @@ export const isCursorInFrame = ( const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); return isPointWithinBounds( - point(fx1, fy1), - point(cursorCoords.x, cursorCoords.y), - point(fx2, fy2), + pointFrom(fx1, fy1), + pointFrom(cursorCoords.x, cursorCoords.y), + pointFrom(fx2, fy2), ); }; diff --git a/packages/excalidraw/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts index 33b57ce686..57b57c570a 100644 --- a/packages/excalidraw/renderer/renderSnaps.ts +++ b/packages/excalidraw/renderer/renderSnaps.ts @@ -1,4 +1,4 @@ -import { point, type GlobalPoint, type LocalPoint } from "../../math"; +import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math"; import { THEME } from "../constants"; import type { PointSnapLine, PointerSnapLine } from "../snapping"; import type { InteractiveCanvasAppState } from "../types"; @@ -140,27 +140,31 @@ const drawGapLine = ( // (1) if (!appState.zenModeEnabled) { drawLine( - point(from[0], from[1] - FULL), - point(from[0], from[1] + FULL), + pointFrom(from[0], from[1] - FULL), + pointFrom(from[0], from[1] + FULL), context, ); } // (3) drawLine( - point(halfPoint[0] - QUARTER, halfPoint[1] - HALF), - point(halfPoint[0] - QUARTER, halfPoint[1] + HALF), + pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF), + pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF), context, ); drawLine( - point(halfPoint[0] + QUARTER, halfPoint[1] - HALF), - point(halfPoint[0] + QUARTER, halfPoint[1] + HALF), + pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF), + pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF), context, ); if (!appState.zenModeEnabled) { // (4) - drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context); + drawLine( + pointFrom(to[0], to[1] - FULL), + pointFrom(to[0], to[1] + FULL), + context, + ); // (2) drawLine(from, to, context); @@ -170,27 +174,31 @@ const drawGapLine = ( // (1) if (!appState.zenModeEnabled) { drawLine( - point(from[0] - FULL, from[1]), - point(from[0] + FULL, from[1]), + pointFrom(from[0] - FULL, from[1]), + pointFrom(from[0] + FULL, from[1]), context, ); } // (3) drawLine( - point(halfPoint[0] - HALF, halfPoint[1] - QUARTER), - point(halfPoint[0] + HALF, halfPoint[1] - QUARTER), + pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER), + pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER), context, ); drawLine( - point(halfPoint[0] - HALF, halfPoint[1] + QUARTER), - point(halfPoint[0] + HALF, halfPoint[1] + QUARTER), + pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER), + pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER), context, ); if (!appState.zenModeEnabled) { // (4) - drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context); + drawLine( + pointFrom(to[0] - FULL, to[1]), + pointFrom(to[0] + FULL, to[1]), + context, + ); // (2) drawLine(from, to, context); diff --git a/packages/excalidraw/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts index fad0f4f938..0426b3f70f 100644 --- a/packages/excalidraw/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -24,7 +24,7 @@ import { import { canChangeRoundness } from "./comparisons"; import type { EmbedsValidationStatus } from "../types"; import { - point, + pointFrom, pointDistance, type GlobalPoint, type LocalPoint, @@ -408,7 +408,7 @@ export const _generateElementShape = ( // initial position to it const points = element.points.length ? element.points - : [point(0, 0)]; + : [pointFrom(0, 0)]; if (isElbowArrow(element)) { shape = [ diff --git a/packages/excalidraw/shapes.tsx b/packages/excalidraw/shapes.tsx index 2c935145c8..3f1855c631 100644 --- a/packages/excalidraw/shapes.tsx +++ b/packages/excalidraw/shapes.tsx @@ -1,6 +1,6 @@ import { isPoint, - point, + pointFrom, pointDistance, pointFromPair, pointRotateRads, @@ -167,15 +167,15 @@ export const getElementShape = ( ? getClosedCurveShape( element, roughShape, - point(element.x, element.y), + pointFrom(element.x, element.y), element.angle, - point(cx, cy), + pointFrom(cx, cy), ) : getCurveShape( roughShape, - point(element.x, element.y), + pointFrom(element.x, element.y), element.angle, - point(cx, cy), + pointFrom(cx, cy), ); } @@ -186,7 +186,7 @@ export const getElementShape = ( const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); return getFreedrawShape( element, - point(cx, cy), + pointFrom(cx, cy), shouldTestInside(element), ); } @@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = < } const ops = getCurvePathOps(shape[0]); - let currentP = point

(0, 0); + let currentP = pointFrom

(0, 0); let index = 0; let minDistance = Infinity; let controlPoints: P[] | null = null; @@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = < } if (op === "bcurveTo") { const p0 = currentP; - const p1 = point

(data[0], data[1]); - const p2 = point

(data[2], data[3]); - const p3 = point

(data[4], data[5]); + const p1 = pointFrom

(data[0], data[1]); + const p2 = pointFrom

(data[2], data[3]); + const p3 = pointFrom

(data[4], data[5]); const distance = pointDistance(p3, endPoint); if (distance < minDistance) { minDistance = distance; @@ -279,7 +279,7 @@ export const getBezierXY =

( p0[idx] * Math.pow(t, 3); const tx = equation(t, 0); const ty = equation(t, 1); - return point(tx, ty); + return pointFrom(tx, ty); }; const getPointsInBezierCurve =

( @@ -301,12 +301,12 @@ const getPointsInBezierCurve =

( controlPoints[3], t, ); - pointsOnCurve.push(point(p[0], p[1])); + pointsOnCurve.push(pointFrom(p[0], p[1])); t -= 0.05; } if (pointsOnCurve.length) { if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) { - pointsOnCurve.push(point(endPoint[0], endPoint[1])); + pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1])); } } return pointsOnCurve; @@ -393,24 +393,24 @@ export const aabbForElement = ( midY: element.y + element.height / 2, }; - const center = point(bbox.midX, bbox.midY); + const center = pointFrom(bbox.midX, bbox.midY); const [topLeftX, topLeftY] = pointRotateRads( - point(bbox.minX, bbox.minY), + pointFrom(bbox.minX, bbox.minY), center, element.angle, ); const [topRightX, topRightY] = pointRotateRads( - point(bbox.maxX, bbox.minY), + pointFrom(bbox.maxX, bbox.minY), center, element.angle, ); const [bottomRightX, bottomRightY] = pointRotateRads( - point(bbox.maxX, bbox.maxY), + pointFrom(bbox.maxX, bbox.maxY), center, element.angle, ); const [bottomLeftX, bottomLeftY] = pointRotateRads( - point(bbox.minX, bbox.maxY), + pointFrom(bbox.minX, bbox.maxY), center, element.angle, ); @@ -442,14 +442,14 @@ export const pointInsideBounds =

( p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; export const aabbsOverlapping = (a: Bounds, b: Bounds) => - pointInsideBounds(point(a[0], a[1]), b) || - pointInsideBounds(point(a[2], a[1]), b) || - pointInsideBounds(point(a[2], a[3]), b) || - pointInsideBounds(point(a[0], a[3]), b) || - pointInsideBounds(point(b[0], b[1]), a) || - pointInsideBounds(point(b[2], b[1]), a) || - pointInsideBounds(point(b[2], b[3]), a) || - pointInsideBounds(point(b[0], b[3]), a); + pointInsideBounds(pointFrom(a[0], a[1]), b) || + pointInsideBounds(pointFrom(a[2], a[1]), b) || + pointInsideBounds(pointFrom(a[2], a[3]), b) || + pointInsideBounds(pointFrom(a[0], a[3]), b) || + pointInsideBounds(pointFrom(b[0], b[1]), a) || + pointInsideBounds(pointFrom(b[2], b[1]), a) || + pointInsideBounds(pointFrom(b[2], b[3]), a) || + pointInsideBounds(pointFrom(b[0], b[3]), a); export const getCornerRadius = (x: number, element: ExcalidrawElement) => { if ( diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 9da3d74c4e..1f2451b337 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -1,6 +1,6 @@ import type { InclusiveRange } from "../math"; import { - point, + pointFrom, pointRotateRads, rangeInclusive, rangeIntersection, @@ -228,52 +228,52 @@ export const getElementsCorners = ( !boundingBoxCorners ) { const leftMid = pointRotateRads( - point(x1, y1 + halfHeight), - point(cx, cy), + pointFrom(x1, y1 + halfHeight), + pointFrom(cx, cy), element.angle, ); const topMid = pointRotateRads( - point(x1 + halfWidth, y1), - point(cx, cy), + pointFrom(x1 + halfWidth, y1), + pointFrom(cx, cy), element.angle, ); const rightMid = pointRotateRads( - point(x2, y1 + halfHeight), - point(cx, cy), + pointFrom(x2, y1 + halfHeight), + pointFrom(cx, cy), element.angle, ); const bottomMid = pointRotateRads( - point(x1 + halfWidth, y2), - point(cx, cy), + pointFrom(x1 + halfWidth, y2), + pointFrom(cx, cy), element.angle, ); - const center = point(cx, cy); + const center = pointFrom(cx, cy); result = omitCenter ? [leftMid, topMid, rightMid, bottomMid] : [leftMid, topMid, rightMid, bottomMid, center]; } else { const topLeft = pointRotateRads( - point(x1, y1), - point(cx, cy), + pointFrom(x1, y1), + pointFrom(cx, cy), element.angle, ); const topRight = pointRotateRads( - point(x2, y1), - point(cx, cy), + pointFrom(x2, y1), + pointFrom(cx, cy), element.angle, ); const bottomLeft = pointRotateRads( - point(x1, y2), - point(cx, cy), + pointFrom(x1, y2), + pointFrom(cx, cy), element.angle, ); const bottomRight = pointRotateRads( - point(x2, y2), - point(cx, cy), + pointFrom(x2, y2), + pointFrom(cx, cy), element.angle, ); - const center = point(cx, cy); + const center = pointFrom(cx, cy); result = omitCenter ? [topLeft, topRight, bottomLeft, bottomRight] @@ -287,18 +287,18 @@ export const getElementsCorners = ( const width = maxX - minX; const height = maxY - minY; - const topLeft = point(minX, minY); - const topRight = point(maxX, minY); - const bottomLeft = point(minX, maxY); - const bottomRight = point(maxX, maxY); - const center = point(minX + width / 2, minY + height / 2); + const topLeft = pointFrom(minX, minY); + const topRight = pointFrom(maxX, minY); + const bottomLeft = pointFrom(minX, maxY); + const bottomRight = pointFrom(maxX, maxY); + const center = pointFrom(minX + width / 2, minY + height / 2); result = omitCenter ? [topLeft, topRight, bottomLeft, bottomRight] : [topLeft, topRight, bottomLeft, bottomRight, center]; } - return result.map((p) => point(round(p[0]), round(p[1]))); + return result.map((p) => pointFrom(round(p[0]), round(p[1]))); }; const getReferenceElements = ( @@ -375,8 +375,11 @@ export const getVisibleGaps = ( horizontalGaps.push({ startBounds, endBounds, - startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)], - endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)], + startSide: [ + pointFrom(startMaxX, startMinY), + pointFrom(startMaxX, startMaxY), + ], + endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)], length: endMinX - startMaxX, overlap: rangeIntersection( rangeInclusive(startMinY, startMaxY), @@ -415,8 +418,11 @@ export const getVisibleGaps = ( verticalGaps.push({ startBounds, endBounds, - startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)], - endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)], + startSide: [ + pointFrom(startMinX, startMaxY), + pointFrom(startMaxX, startMaxY), + ], + endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)], length: endMinY - startMaxY, overlap: rangeIntersection( rangeInclusive(startMinX, startMaxX), @@ -832,7 +838,7 @@ const createPointSnapLines = ( } snapsX[key].push( ...snap.points.map((p) => - point(round(p[0]), round(p[1])), + pointFrom(round(p[0]), round(p[1])), ), ); } @@ -849,7 +855,7 @@ const createPointSnapLines = ( } snapsY[key].push( ...snap.points.map((p) => - point(round(p[0]), round(p[1])), + pointFrom(round(p[0]), round(p[1])), ), ); } @@ -863,7 +869,7 @@ const createPointSnapLines = ( points: dedupePoints( points .map((p) => { - return point(Number(key), p[1]); + return pointFrom(Number(key), p[1]); }) .sort((a, b) => a[1] - b[1]), ), @@ -876,7 +882,7 @@ const createPointSnapLines = ( points: dedupePoints( points .map((p) => { - return point(p[0], Number(key)); + return pointFrom(p[0], Number(key)); }) .sort((a, b) => a[0] - b[0]), ), @@ -940,16 +946,16 @@ const createGapSnapLines = ( type: "gap", direction: "horizontal", points: [ - point(gapSnap.gap.startSide[0][0], gapLineY), - point(minX, gapLineY), + pointFrom(gapSnap.gap.startSide[0][0], gapLineY), + pointFrom(minX, gapLineY), ], }, { type: "gap", direction: "horizontal", points: [ - point(maxX, gapLineY), - point(gapSnap.gap.endSide[0][0], gapLineY), + pointFrom(maxX, gapLineY), + pointFrom(gapSnap.gap.endSide[0][0], gapLineY), ], }, ); @@ -966,16 +972,16 @@ const createGapSnapLines = ( type: "gap", direction: "vertical", points: [ - point(gapLineX, gapSnap.gap.startSide[0][1]), - point(gapLineX, minY), + pointFrom(gapLineX, gapSnap.gap.startSide[0][1]), + pointFrom(gapLineX, minY), ], }, { type: "gap", direction: "vertical", points: [ - point(gapLineX, maxY), - point(gapLineX, gapSnap.gap.endSide[0][1]), + pointFrom(gapLineX, maxY), + pointFrom(gapLineX, gapSnap.gap.endSide[0][1]), ], }, ); @@ -991,12 +997,15 @@ const createGapSnapLines = ( { type: "gap", direction: "horizontal", - points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], + points: [ + pointFrom(startMaxX, gapLineY), + pointFrom(endMinX, gapLineY), + ], }, { type: "gap", direction: "horizontal", - points: [point(endMaxX, gapLineY), point(minX, gapLineY)], + points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)], }, ); } @@ -1011,12 +1020,18 @@ const createGapSnapLines = ( { type: "gap", direction: "horizontal", - points: [point(maxX, gapLineY), point(startMinX, gapLineY)], + points: [ + pointFrom(maxX, gapLineY), + pointFrom(startMinX, gapLineY), + ], }, { type: "gap", direction: "horizontal", - points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], + points: [ + pointFrom(startMaxX, gapLineY), + pointFrom(endMinX, gapLineY), + ], }, ); } @@ -1031,12 +1046,18 @@ const createGapSnapLines = ( { type: "gap", direction: "vertical", - points: [point(gapLineX, maxY), point(gapLineX, startMinY)], + points: [ + pointFrom(gapLineX, maxY), + pointFrom(gapLineX, startMinY), + ], }, { type: "gap", direction: "vertical", - points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], + points: [ + pointFrom(gapLineX, startMaxY), + pointFrom(gapLineX, endMinY), + ], }, ); } @@ -1051,12 +1072,15 @@ const createGapSnapLines = ( { type: "gap", direction: "vertical", - points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], + points: [ + pointFrom(gapLineX, startMaxY), + pointFrom(gapLineX, endMinY), + ], }, { type: "gap", direction: "vertical", - points: [point(gapLineX, endMaxY), point(gapLineX, minY)], + points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)], }, ); } @@ -1070,7 +1094,7 @@ const createGapSnapLines = ( return { ...gapSnapLine, points: gapSnapLine.points.map((p) => - point(round(p[0]), round(p[1])), + pointFrom(round(p[0]), round(p[1])), ) as PointPair, }; }), @@ -1120,35 +1144,35 @@ export const snapResizingElements = ( if (transformHandle) { switch (transformHandle) { case "e": { - selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY)); break; } case "w": { - selectionSnapPoints.push(point(minX, minY), point(minX, maxY)); + selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY)); break; } case "n": { - selectionSnapPoints.push(point(minX, minY), point(maxX, minY)); + selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY)); break; } case "s": { - selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY)); break; } case "ne": { - selectionSnapPoints.push(point(maxX, minY)); + selectionSnapPoints.push(pointFrom(maxX, minY)); break; } case "nw": { - selectionSnapPoints.push(point(minX, minY)); + selectionSnapPoints.push(pointFrom(minX, minY)); break; } case "se": { - selectionSnapPoints.push(point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(maxX, maxY)); break; } case "sw": { - selectionSnapPoints.push(point(minX, maxY)); + selectionSnapPoints.push(pointFrom(minX, maxY)); break; } } @@ -1191,10 +1215,10 @@ export const snapResizingElements = ( ); const corners: GlobalPoint[] = [ - point(x1, y1), - point(x1, y2), - point(x2, y1), - point(x2, y2), + pointFrom(x1, y1), + pointFrom(x1, y2), + pointFrom(x2, y1), + pointFrom(x2, y2), ]; getPointSnaps( @@ -1231,7 +1255,7 @@ export const snapNewElement = ( } const selectionSnapPoints: GlobalPoint[] = [ - point(origin.x + dragOffset.x, origin.y + dragOffset.y), + pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y), ]; const snapDistance = getSnapDistance(app.state.zoom.value); @@ -1331,7 +1355,7 @@ export const getSnapLinesAtPointer = ( verticalSnapLines.push({ type: "pointer", - points: [corner, point(corner[0], pointer.y)], + points: [corner, pointFrom(corner[0], pointer.y)], direction: "vertical", }); @@ -1347,7 +1371,7 @@ export const getSnapLinesAtPointer = ( horizontalSnapLines.push({ type: "pointer", - points: [corner, point(pointer.x, corner[1])], + points: [corner, pointFrom(pointer.x, corner[1])], direction: "horizontal", }); diff --git a/packages/excalidraw/tests/binding.test.tsx b/packages/excalidraw/tests/binding.test.tsx index 4f6d6b56bb..680cbfa857 100644 --- a/packages/excalidraw/tests/binding.test.tsx +++ b/packages/excalidraw/tests/binding.test.tsx @@ -7,7 +7,7 @@ import { API } from "./helpers/api"; import { KEYS } from "../keys"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { arrayToMap } from "../utils"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const { h } = window; @@ -32,7 +32,12 @@ describe("element binding", () => { y: 0, width: 100, height: 1, - points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)], + points: [ + pointFrom(0, 0), + pointFrom(0, 0), + pointFrom(100, 0), + pointFrom(100, 0), + ], }); API.setElements([rect, arrow]); expect(arrow.startBinding).toBe(null); @@ -310,7 +315,7 @@ describe("element binding", () => { const arrow1 = API.createElement({ type: "arrow", id: "arrow1", - points: [point(0, 0), point(0, -87.45777932247563)], + points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], startBinding: { elementId: "rectangle1", focus: 0.2, @@ -328,7 +333,7 @@ describe("element binding", () => { const arrow2 = API.createElement({ type: "arrow", id: "arrow2", - points: [point(0, 0), point(0, -87.45777932247563)], + points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], startBinding: { elementId: "text1", focus: 0.2, diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index 53cbc53c8b..03a2ac1fbf 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -28,7 +28,7 @@ import { getBoundTextElementPosition } from "../element/textElement"; import { createPasteEvent } from "../clipboard"; import { arrayToMap, cloneJSON } from "../utils"; import type { LocalPoint } from "../../math"; -import { point, type Radians } from "../../math"; +import { pointFrom, type Radians } from "../../math"; const { h } = window; const mouse = new Pointer("mouse"); @@ -146,9 +146,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = ( link: null, locked: false, points: [ - point(0, 0), - point(-922.4761962890625, 300.3277587890625), - point(828.0126953125, 410.51605224609375), + pointFrom(0, 0), + pointFrom(-922.4761962890625, 300.3277587890625), + pointFrom(828.0126953125, 410.51605224609375), ], }); }; diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index b7dc6e10d6..66c9408608 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -38,7 +38,7 @@ import type App from "../../components/App"; import { createTestHook } from "../../components/App"; import type { Action } from "../../actions/types"; import { mutateElement } from "../../element/mutateElement"; -import { point, type LocalPoint, type Radians } from "../../../math"; +import { pointFrom, type LocalPoint, type Radians } from "../../../math"; const readFile = util.promisify(fs.readFile); // so that window.h is available when App.tsx is not imported as well. @@ -307,8 +307,8 @@ export class API { height, type, points: rest.points ?? [ - point(0, 0), - point(100, 100), + pointFrom(0, 0), + pointFrom(100, 100), ], elbowed: rest.elbowed ?? false, }); @@ -320,8 +320,8 @@ export class API { height, type, points: rest.points ?? [ - point(0, 0), - point(100, 100), + pointFrom(0, 0), + pointFrom(100, 100), ], }); break; diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index 3c0cd769ca..721982212c 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -34,7 +34,7 @@ import { getTextEditor } from "../queries/dom"; import { arrayToMap } from "../../utils"; import { createTestHook } from "../../components/App"; import type { GlobalPoint, LocalPoint, Radians } from "../../../math"; -import { point, pointRotateRads } from "../../../math"; +import { pointFrom, pointRotateRads } from "../../../math"; // so that window.h is available when App.tsx is not imported as well. createTestHook(); @@ -142,7 +142,7 @@ const getElementPointForSelection = ( element: ExcalidrawElement, ): GlobalPoint => { const { x, y, width, height, angle } = element; - const target = point( + const target = pointFrom( x + (isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2), y, @@ -151,9 +151,12 @@ const getElementPointForSelection = ( if (isLinearElement(element)) { const bounds = getElementPointsCoords(element, element.points); - center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2); + center = pointFrom( + (bounds[0] + bounds[2]) / 2, + (bounds[1] + bounds[3]) / 2, + ); } else { - center = point(x + width / 2, y + height / 2); + center = pointFrom(x + width / 2, y + height / 2); } if (isTextElement(element)) { @@ -469,8 +472,8 @@ export class UI { const width = initialWidth ?? initialHeight ?? size; const height = initialHeight ?? size; const points: LocalPoint[] = initialPoints ?? [ - point(0, 0), - point(width, height), + pointFrom(0, 0), + pointFrom(width, height), ]; UI.clickTool(type); diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 3c807cf915..6a5db9753f 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -46,7 +46,7 @@ import { HistoryEntry } from "../history"; import { AppStateChange, ElementsChange } from "../change"; import { Snapshot, StoreAction } from "../store"; import type { LocalPoint, Radians } from "../../math"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const { h } = window; @@ -2041,9 +2041,9 @@ describe("history", () => { width: 178.9000000000001, height: 236.10000000000002, points: [ - point(0, 0), - point(178.9000000000001, 0), - point(178.9000000000001, 236.10000000000002), + pointFrom(0, 0), + pointFrom(178.9000000000001, 0), + pointFrom(178.9000000000001, 236.10000000000002), ], startBinding: { elementId: "KPrBI4g_v9qUB1XxYLgSz", @@ -2159,11 +2159,11 @@ describe("history", () => { elements: [ newElementWith(h.elements[0] as ExcalidrawLinearElement, { points: [ - point(0, 0), - point(5, 5), - point(10, 10), - point(15, 15), - point(20, 20), + pointFrom(0, 0), + pointFrom(5, 5), + pointFrom(10, 10), + pointFrom(15, 15), + pointFrom(20, 20), ] as LocalPoint[], }), ], diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index 06ca24a9cb..5df260d1d5 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -28,7 +28,7 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { vi } from "vitest"; import { arrayToMap } from "../utils"; import type { GlobalPoint } from "../../math"; -import { pointCenter, point } from "../../math"; +import { pointCenter, pointFrom } from "../../math"; const renderInteractiveScene = vi.spyOn( InteractiveCanvas, @@ -57,8 +57,8 @@ describe("Test Linear Elements", () => { interactiveCanvas = container.querySelector("canvas.interactive")!; }); - const p1 = point(20, 20); - const p2 = point(60, 20); + const p1 = pointFrom(20, 20); + const p2 = pointFrom(60, 20); const midpoint = pointCenter(p1, p2); const delta = 50; const mouse = new Pointer("mouse"); @@ -75,7 +75,7 @@ describe("Test Linear Elements", () => { height: 0, type, roughness, - points: [point(0, 0), point(p2[0] - p1[0], p2[1] - p1[1])], + points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])], roundness, }); API.setElements([line]); @@ -99,9 +99,9 @@ describe("Test Linear Elements", () => { type, roughness, points: [ - point(0, 0), - point(p3[0], p3[1]), - point(p2[0] - p1[0], p2[1] - p1[1]), + pointFrom(0, 0), + pointFrom(p3[0], p3[1]), + pointFrom(p2[0] - p1[0], p2[1] - p1[1]), ], roundness, }); @@ -161,7 +161,7 @@ describe("Test Linear Elements", () => { expect(line.points.length).toEqual(2); mouse.clickAt(midpoint[0], midpoint[1]); - drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1)); + drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); expect(line.points.length).toEqual(2); @@ -169,7 +169,7 @@ describe("Test Linear Elements", () => { expect(line.y).toBe(originalY); expect(line.points.length).toEqual(2); - drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); + drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); expect(line.x).toBe(originalX); expect(line.y).toBe(originalY); expect(line.points.length).toEqual(3); @@ -184,7 +184,7 @@ describe("Test Linear Elements", () => { expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); // drag line from midpoint - drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); + drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(line.points.length).toEqual(3); @@ -248,7 +248,7 @@ describe("Test Linear Elements", () => { mouse.clickAt(midpoint[0], midpoint[1]); expect(line.points.length).toEqual(2); - drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1)); + drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); expect(line.x).toBe(originalX); expect(line.y).toBe(originalY); expect(line.points.length).toEqual(3); @@ -261,7 +261,7 @@ describe("Test Linear Elements", () => { enterLineEditingMode(line); // drag line from midpoint - drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); + drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( `12`, ); @@ -356,7 +356,7 @@ describe("Test Linear Elements", () => { const startPoint = pointCenter(points[0], midPoints[0]!); const deltaX = 50; const deltaY = 20; - const endPoint = point( + const endPoint = pointFrom( startPoint[0] + deltaX, startPoint[1] + deltaY, ); @@ -399,8 +399,8 @@ describe("Test Linear Elements", () => { // This is the expected midpoint for line with round edge // hence hardcoding it so if later some bug is introduced // this will fail and we can fix it - const firstSegmentMidpoint = point(55, 45); - const lastSegmentMidpoint = point(75, 40); + const firstSegmentMidpoint = pointFrom(55, 45); + const lastSegmentMidpoint = pointFrom(75, 40); let line: ExcalidrawLinearElement; @@ -416,7 +416,7 @@ describe("Test Linear Elements", () => { // drag line via first segment midpoint drag( firstSegmentMidpoint, - point( + pointFrom( firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[1] + delta, ), @@ -426,7 +426,10 @@ describe("Test Linear Elements", () => { // drag line from last segment midpoint drag( lastSegmentMidpoint, - point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), ); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( @@ -475,10 +478,10 @@ describe("Test Linear Elements", () => { h.state, ); - const hitCoords = point(points[0][0], points[0][1]); + const hitCoords = pointFrom(points[0][0], points[0][1]); // Drag from first point - drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta)); + drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( `12`, @@ -516,10 +519,10 @@ describe("Test Linear Elements", () => { h.state, ); - const hitCoords = point(points[0][0], points[0][1]); + const hitCoords = pointFrom(points[0][0], points[0][1]); // Drag from first point - drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta)); + drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( `12`, @@ -556,7 +559,7 @@ describe("Test Linear Elements", () => { // dragging line from last segment midpoint drag( lastSegmentMidpoint, - point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50), + pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50), ); expect(line.points.length).toEqual(4); @@ -589,11 +592,11 @@ describe("Test Linear Elements", () => { // This is the expected midpoint for line with round edge // hence hardcoding it so if later some bug is introduced // this will fail and we can fix it - const firstSegmentMidpoint = point( + const firstSegmentMidpoint = pointFrom( 55.9697848965255, 47.442326230998205, ); - const lastSegmentMidpoint = point( + const lastSegmentMidpoint = pointFrom( 76.08587175006699, 43.294165939653226, ); @@ -612,7 +615,7 @@ describe("Test Linear Elements", () => { // drag line from first segment midpoint drag( firstSegmentMidpoint, - point( + pointFrom( firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[1] + delta, ), @@ -622,7 +625,10 @@ describe("Test Linear Elements", () => { // drag line from last segment midpoint drag( lastSegmentMidpoint, - point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), ); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( `16`, @@ -669,10 +675,10 @@ describe("Test Linear Elements", () => { h.state, ); - const hitCoords = point(points[0][0], points[0][1]); + const hitCoords = pointFrom(points[0][0], points[0][1]); // Drag from first point - drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta)); + drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); const newPoints = LinearElementEditor.getPointsGlobalCoordinates( line, @@ -717,10 +723,10 @@ describe("Test Linear Elements", () => { h.state, ); - const hitCoords = point(points[0][0], points[0][1]); + const hitCoords = pointFrom(points[0][0], points[0][1]); // Drag from first point - drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta)); + drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( `12`, @@ -751,7 +757,10 @@ describe("Test Linear Elements", () => { drag( lastSegmentMidpoint, - point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), ); expect(line.points.length).toEqual(4); @@ -811,8 +820,8 @@ describe("Test Linear Elements", () => { API.setSelectedElements([line]); enterLineEditingMode(line, true); drag( - point(line.points[0][0] + line.x, line.points[0][1] + line.y), - point( + pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y), + pointFrom( dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y, ), @@ -927,14 +936,14 @@ describe("Test Linear Elements", () => { // This is the expected midpoint for line with round edge // hence hardcoding it so if later some bug is introduced // this will fail and we can fix it - const firstSegmentMidpoint = point( + const firstSegmentMidpoint = pointFrom( 55.9697848965255, 47.442326230998205, ); // drag line from first segment midpoint drag( firstSegmentMidpoint, - point( + pointFrom( firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[1] + delta, ), @@ -1151,7 +1160,7 @@ describe("Test Linear Elements", () => { ); // Drag from last point - drag(points[1], point(points[1][0] + 300, points[1][1])); + drag(points[1], pointFrom(points[1][0] + 300, points[1][1])); expect({ width: container.width, height: container.height }) .toMatchInlineSnapshot(` @@ -1350,11 +1359,11 @@ describe("Test Linear Elements", () => { [ { index: 0, - point: point(line.points[0][0] + 10, line.points[0][1] + 10), + point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10), }, { index: line.points.length - 1, - point: point( + point: pointFrom( line.points[line.points.length - 1][0] - 10, line.points[line.points.length - 1][1] - 10, ), diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/excalidraw/tests/resize.test.tsx index 8de7157b18..05f8627a83 100644 --- a/packages/excalidraw/tests/resize.test.tsx +++ b/packages/excalidraw/tests/resize.test.tsx @@ -17,7 +17,7 @@ import { isLinearElement } from "../element/typeChecks"; import { LinearElementEditor } from "../element/linearElementEditor"; import { arrayToMap } from "../utils"; import type { LocalPoint } from "../../math"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -220,12 +220,17 @@ describe("generic element", () => { describe.each(["line", "freedraw"] as const)("%s element", (type) => { const points: Record = { - line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)], + line: [ + pointFrom(0, 0), + pointFrom(60, -20), + pointFrom(20, 40), + pointFrom(-40, 0), + ], freedraw: [ - point(0, 0), - point(-2.474600807561444, 41.021700699972), - point(3.6627956000014024, 47.84174560617245), - point(40.495224145598115, 47.15909710753482), + pointFrom(0, 0), + pointFrom(-2.474600807561444, 41.021700699972), + pointFrom(3.6627956000014024, 47.84174560617245), + pointFrom(40.495224145598115, 47.15909710753482), ], }; @@ -293,11 +298,11 @@ describe("arrow element", () => { it("resizes with a label", async () => { const arrow = UI.createElement("arrow", { points: [ - point(0, 0), - point(40, 140), - point(80, 60), // label's anchor - point(180, 20), - point(200, 120), + pointFrom(0, 0), + pointFrom(40, 140), + pointFrom(80, 60), // label's anchor + pointFrom(180, 20), + pointFrom(200, 120), ], }); const label = await UI.editText(arrow, "Hello"); @@ -747,24 +752,24 @@ describe("multiple selection", () => { x: 60, y: 40, points: [ - point(0, 0), - point(-40, 40), - point(-60, 0), - point(0, -40), - point(40, 20), - point(0, 40), + pointFrom(0, 0), + pointFrom(-40, 40), + pointFrom(-60, 0), + pointFrom(0, -40), + pointFrom(40, 20), + pointFrom(0, 40), ], }); const freedraw = UI.createElement("freedraw", { x: 63.56072661326618, y: 100, points: [ - point(0, 0), - point(-43.56072661326618, 18.15048126846341), - point(-43.56072661326618, 29.041198460587566), - point(-38.115368017204105, 42.652452795512204), - point(-19.964886748740696, 66.24829266003775), - point(19.056612930986716, 77.1390098521619), + pointFrom(0, 0), + pointFrom(-43.56072661326618, 18.15048126846341), + pointFrom(-43.56072661326618, 29.041198460587566), + pointFrom(-38.115368017204105, 42.652452795512204), + pointFrom(-19.964886748740696, 66.24829266003775), + pointFrom(19.056612930986716, 77.1390098521619), ], }); @@ -1101,13 +1106,13 @@ describe("multiple selection", () => { x: 60, y: 0, points: [ - point(0, 0), - point(-40, 40), - point(-20, 60), - point(20, 20), - point(40, 40), - point(-20, 100), - point(-60, 60), + pointFrom(0, 0), + pointFrom(-40, 40), + pointFrom(-20, 60), + pointFrom(20, 20), + pointFrom(40, 40), + pointFrom(-20, 100), + pointFrom(-60, 60), ], }); diff --git a/packages/excalidraw/visualdebug.ts b/packages/excalidraw/visualdebug.ts index 86f4d39a82..baddeeadcf 100644 --- a/packages/excalidraw/visualdebug.ts +++ b/packages/excalidraw/visualdebug.ts @@ -1,4 +1,9 @@ -import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math"; +import { + isLineSegment, + lineSegment, + pointFrom, + type GlobalPoint, +} from "../math"; import type { LineSegment } from "../utils"; import type { BoundingBox, Bounds } from "./element/bounds"; import { isBounds } from "./element/typeChecks"; @@ -52,8 +57,8 @@ export const debugDrawPoint = ( debugDrawLine( lineSegment( - point(p[0] + xOffset - 10, p[1] + yOffset - 10), - point(p[0] + xOffset + 10, p[1] + yOffset + 10), + pointFrom(p[0] + xOffset - 10, p[1] + yOffset - 10), + pointFrom(p[0] + xOffset + 10, p[1] + yOffset + 10), ), { color: opts?.color ?? "cyan", @@ -62,8 +67,8 @@ export const debugDrawPoint = ( ); debugDrawLine( lineSegment( - point(p[0] + xOffset - 10, p[1] + yOffset + 10), - point(p[0] + xOffset + 10, p[1] + yOffset - 10), + pointFrom(p[0] + xOffset - 10, p[1] + yOffset + 10), + pointFrom(p[0] + xOffset + 10, p[1] + yOffset - 10), ), { color: opts?.color ?? "cyan", @@ -83,20 +88,20 @@ export const debugDrawBoundingBox = ( debugDrawLine( [ lineSegment( - point(bbox.minX, bbox.minY), - point(bbox.maxX, bbox.minY), + pointFrom(bbox.minX, bbox.minY), + pointFrom(bbox.maxX, bbox.minY), ), lineSegment( - point(bbox.maxX, bbox.minY), - point(bbox.maxX, bbox.maxY), + pointFrom(bbox.maxX, bbox.minY), + pointFrom(bbox.maxX, bbox.maxY), ), lineSegment( - point(bbox.maxX, bbox.maxY), - point(bbox.minX, bbox.maxY), + pointFrom(bbox.maxX, bbox.maxY), + pointFrom(bbox.minX, bbox.maxY), ), lineSegment( - point(bbox.minX, bbox.maxY), - point(bbox.minX, bbox.minY), + pointFrom(bbox.minX, bbox.maxY), + pointFrom(bbox.minX, bbox.minY), ), ], { @@ -118,20 +123,20 @@ export const debugDrawBounds = ( debugDrawLine( [ lineSegment( - point(bbox[0], bbox[1]), - point(bbox[2], bbox[1]), + pointFrom(bbox[0], bbox[1]), + pointFrom(bbox[2], bbox[1]), ), lineSegment( - point(bbox[2], bbox[1]), - point(bbox[2], bbox[3]), + pointFrom(bbox[2], bbox[1]), + pointFrom(bbox[2], bbox[3]), ), lineSegment( - point(bbox[2], bbox[3]), - point(bbox[0], bbox[3]), + pointFrom(bbox[2], bbox[3]), + pointFrom(bbox[0], bbox[3]), ), lineSegment( - point(bbox[0], bbox[3]), - point(bbox[0], bbox[1]), + pointFrom(bbox[0], bbox[3]), + pointFrom(bbox[0], bbox[1]), ), ], { diff --git a/packages/math/arc.test.ts b/packages/math/arc.test.ts index 12e880c9c1..adf7785916 100644 --- a/packages/math/arc.test.ts +++ b/packages/math/arc.test.ts @@ -1,5 +1,5 @@ import { isPointOnSymmetricArc } from "./arc"; -import { point } from "./point"; +import { pointFrom } from "./point"; describe("point on arc", () => { it("should detect point on simple arc", () => { @@ -10,7 +10,7 @@ describe("point on arc", () => { startAngle: -Math.PI / 4, endAngle: Math.PI / 4, }, - point(0.92291667, 0.385), + pointFrom(0.92291667, 0.385), ), ).toBe(true); }); @@ -22,7 +22,7 @@ describe("point on arc", () => { startAngle: -Math.PI / 4, endAngle: Math.PI / 4, }, - point(-0.92291667, 0.385), + pointFrom(-0.92291667, 0.385), ), ).toBe(false); }); @@ -34,7 +34,7 @@ describe("point on arc", () => { startAngle: -Math.PI / 4, endAngle: Math.PI / 4, }, - point(-0.5, 0.5), + pointFrom(-0.5, 0.5), ), ).toBe(false); }); diff --git a/packages/math/curve.ts b/packages/math/curve.ts index ca4571057d..68a885fd82 100644 --- a/packages/math/curve.ts +++ b/packages/math/curve.ts @@ -1,4 +1,4 @@ -import { point, pointRotateRads } from "./point"; +import { pointFrom, pointRotateRads } from "./point"; import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types"; /** @@ -43,10 +43,10 @@ export function curveToBezier( const out: Point[] = []; if (len === 3) { out.push( - point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned - point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned - point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned - point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned + pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned + pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned + pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned + pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned ); } else { const points: Point[] = []; @@ -59,19 +59,19 @@ export function curveToBezier( } const b: Point[] = []; const s = 1 - curveTightness; - out.push(point(points[0][0], points[0][1])); + out.push(pointFrom(points[0][0], points[0][1])); for (let i = 1; i + 2 < points.length; i++) { const cachedVertArray = points[i]; - b[0] = point(cachedVertArray[0], cachedVertArray[1]); - b[1] = point( + b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]); + b[1] = pointFrom( cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, ); - b[2] = point( + b[2] = pointFrom( points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, ); - b[3] = point(points[i + 1][0], points[i + 1][1]); + b[3] = pointFrom(points[i + 1][0], points[i + 1][1]); out.push(b[1], b[2], b[3]); } } @@ -102,7 +102,7 @@ export const cubicBezierPoint = ( 3 * (1 - t) * Math.pow(t, 2) * p2[1] + Math.pow(t, 3) * p3[1]; - return point(x, y); + return pointFrom(x, y); }; /** diff --git a/packages/math/point.test.ts b/packages/math/point.test.ts index 77ea06c932..89cc4f8f38 100644 --- a/packages/math/point.test.ts +++ b/packages/math/point.test.ts @@ -1,4 +1,4 @@ -import { point, pointRotateRads } from "./point"; +import { pointFrom, pointRotateRads } from "./point"; import type { Radians } from "./types"; describe("rotate", () => { @@ -9,14 +9,14 @@ describe("rotate", () => { const y2 = 30; const angle = (Math.PI / 2) as Radians; const [rotatedX, rotatedY] = pointRotateRads( - point(x1, y1), - point(x2, y2), + pointFrom(x1, y1), + pointFrom(x2, y2), angle, ); expect([rotatedX, rotatedY]).toEqual([30, 20]); const res2 = pointRotateRads( - point(rotatedX, rotatedY), - point(x2, y2), + pointFrom(rotatedX, rotatedY), + pointFrom(x2, y2), -angle as Radians, ); expect(res2).toEqual([x1, x2]); diff --git a/packages/math/point.ts b/packages/math/point.ts index 97b5742707..61de8f139e 100644 --- a/packages/math/point.ts +++ b/packages/math/point.ts @@ -16,7 +16,7 @@ import { vectorFromPoint, vectorScale } from "./vector"; * @param y The Y coordinate * @returns The branded and created point */ -export function point( +export function pointFrom( x: number, y: number, ): Point { @@ -33,7 +33,7 @@ export function pointFromArray( numberArray: number[], ): Point | undefined { return numberArray.length === 2 - ? point(numberArray[0], numberArray[1]) + ? pointFrom(numberArray[0], numberArray[1]) : undefined; } @@ -107,7 +107,7 @@ export function pointRotateRads( [cx, cy]: Point, angle: Radians, ): Point { - return point( + return pointFrom( (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx, (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy, ); @@ -146,7 +146,7 @@ export function pointTranslate< From extends GlobalPoint | LocalPoint, To extends GlobalPoint | LocalPoint, >(p: From, v: Vector = [0, 0] as Vector): To { - return point(p[0] + v[0], p[1] + v[1]); + return pointFrom(p[0] + v[0], p[1] + v[1]); } /** @@ -157,7 +157,7 @@ export function pointTranslate< * @returns The middle point */ export function pointCenter

(a: P, b: P): P { - return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); + return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); } /** @@ -172,7 +172,7 @@ export function pointAdd( a: Point, b: Point, ): Point { - return point(a[0] + b[0], a[1] + b[1]); + return pointFrom(a[0] + b[0], a[1] + b[1]); } /** @@ -187,7 +187,7 @@ export function pointSubtract( a: Point, b: Point, ): Point { - return point(a[0] - b[0], a[1] - b[1]); + return pointFrom(a[0] - b[0], a[1] - b[1]); } /** diff --git a/packages/utils/collision.test.ts b/packages/utils/collision.test.ts index 398c3cb680..300b5acc58 100644 --- a/packages/utils/collision.test.ts +++ b/packages/utils/collision.test.ts @@ -4,7 +4,7 @@ import { degreesToRadians, lineSegment, lineSegmentRotate, - point, + pointFrom, pointRotateDegs, } from "../math"; import { pointOnCurve, pointOnPolyline } from "./collision"; @@ -12,21 +12,21 @@ import type { Polyline } from "./geometry/shape"; describe("point and curve", () => { const c: Curve = curve( - point(1.4, 1.65), - point(1.9, 7.9), - point(5.9, 1.65), - point(6.44, 4.84), + pointFrom(1.4, 1.65), + pointFrom(1.9, 7.9), + pointFrom(5.9, 1.65), + pointFrom(6.44, 4.84), ); it("point on curve", () => { expect(pointOnCurve(c[0], c, 10e-5)).toBe(true); expect(pointOnCurve(c[3], c, 10e-5)).toBe(true); - expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true); - expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true); - expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true); + expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true); + expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true); + expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true); - expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false); + expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false); expect(pointOnCurve(c[1], c, 0.1)).toBe(false); expect(pointOnCurve(c[2], c, 0.1)).toBe(false); }); @@ -34,52 +34,52 @@ describe("point and curve", () => { describe("point and polylines", () => { const polyline: Polyline = [ - lineSegment(point(1, 0), point(1, 2)), - lineSegment(point(1, 2), point(2, 2)), - lineSegment(point(2, 2), point(2, 1)), - lineSegment(point(2, 1), point(3, 1)), + lineSegment(pointFrom(1, 0), pointFrom(1, 2)), + lineSegment(pointFrom(1, 2), pointFrom(2, 2)), + lineSegment(pointFrom(2, 2), pointFrom(2, 1)), + lineSegment(pointFrom(2, 1), pointFrom(3, 1)), ]; it("point on the line", () => { - expect(pointOnPolyline(point(1, 0), polyline)).toBe(true); - expect(pointOnPolyline(point(1, 2), polyline)).toBe(true); - expect(pointOnPolyline(point(2, 2), polyline)).toBe(true); - expect(pointOnPolyline(point(2, 1), polyline)).toBe(true); - expect(pointOnPolyline(point(3, 1), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true); - expect(pointOnPolyline(point(1, 1), polyline)).toBe(true); - expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true); - expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true); + expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true); - expect(pointOnPolyline(point(0, 1), polyline)).toBe(false); - expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false); + expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false); + expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false); }); it("point on the line with rotation", () => { const truePoints = [ - point(1, 0), - point(1, 2), - point(2, 2), - point(2, 1), - point(3, 1), + pointFrom(1, 0), + pointFrom(1, 2), + pointFrom(2, 2), + pointFrom(2, 1), + pointFrom(3, 1), ]; truePoints.forEach((p) => { const rotation = (Math.random() * 360) as Degrees; - const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); + const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); const rotatedPolyline = polyline.map((line) => - lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), + lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), ); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true); }); - const falsePoints = [point(0, 1), point(2.1, 1.5)]; + const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)]; falsePoints.forEach((p) => { const rotation = (Math.random() * 360) as Degrees; - const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); + const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); const rotatedPolyline = polyline.map((line) => - lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), + lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), ); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false); }); diff --git a/packages/utils/collision.ts b/packages/utils/collision.ts index 1269397379..5af742b508 100644 --- a/packages/utils/collision.ts +++ b/packages/utils/collision.ts @@ -7,7 +7,7 @@ import { import type { Curve } from "../math"; import { lineSegment, - point, + pointFrom, polygonIncludesPoint, pointOnLineSegment, pointOnPolygon, @@ -110,7 +110,7 @@ const polyLineFromCurve = ( for (let i = 0; i < segments; i++) { t += increment; if (t <= 1) { - const nextPoint: Point = point(equation(t, 0), equation(t, 1)); + const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1)); lineSegments.push(lineSegment(startingPoint, nextPoint)); startingPoint = nextPoint; } diff --git a/packages/utils/geometry/geometry.test.ts b/packages/utils/geometry/geometry.test.ts index 6ee357d707..3f425d0564 100644 --- a/packages/utils/geometry/geometry.test.ts +++ b/packages/utils/geometry/geometry.test.ts @@ -1,6 +1,6 @@ import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math"; import { - point, + pointFrom, lineSegment, polygon, pointOnLineSegment, @@ -23,93 +23,127 @@ describe("point and line", () => { // expect(pointRightofLine(point(2, 1), l)).toBe(true); // }); - const s: LineSegment = lineSegment(point(1, 0), point(1, 2)); + const s: LineSegment = lineSegment( + pointFrom(1, 0), + pointFrom(1, 2), + ); it("point on the line", () => { - expect(pointOnLineSegment(point(0, 1), s)).toBe(false); - expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true); - expect(pointOnLineSegment(point(2, 1), s)).toBe(false); + expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false); + expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true); + expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false); }); }); describe("point and polygon", () => { const poly: Polygon = polygon( - point(10, 10), - point(50, 10), - point(50, 50), - point(10, 50), + pointFrom(10, 10), + pointFrom(50, 10), + pointFrom(50, 50), + pointFrom(10, 50), ); it("point on polygon", () => { - expect(pointOnPolygon(point(30, 10), poly)).toBe(true); - expect(pointOnPolygon(point(50, 30), poly)).toBe(true); - expect(pointOnPolygon(point(30, 50), poly)).toBe(true); - expect(pointOnPolygon(point(10, 30), poly)).toBe(true); - expect(pointOnPolygon(point(30, 30), poly)).toBe(false); - expect(pointOnPolygon(point(30, 70), poly)).toBe(false); + expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true); + expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true); + expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true); + expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true); + expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false); + expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false); }); it("point in polygon", () => { const poly: Polygon = polygon( - point(0, 0), - point(2, 0), - point(2, 2), - point(0, 2), + pointFrom(0, 0), + pointFrom(2, 0), + pointFrom(2, 2), + pointFrom(0, 2), ); - expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true); - expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false); + expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true); + expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false); }); }); describe("point and ellipse", () => { const ellipse: Ellipse = { - center: point(0, 0), + center: pointFrom(0, 0), angle: 0 as Radians, halfWidth: 2, halfHeight: 1, }; 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(point(-1.4, 0.7), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true); + expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true); - expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true); + expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true); - expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true); + expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true); - expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true); + expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true); - expect(pointOnEllipse(point(-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(pointFrom(1, -0.8), ellipse)).toBe(false); }); 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(point(-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(pointFrom(1, -0.8), ellipse)).toBe(true); - expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false); - expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false); + expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false); + expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false); }); }); describe("line and line", () => { - const lineA: LineSegment = lineSegment(point(1, 4), point(3, 4)); - const lineB: LineSegment = lineSegment(point(2, 1), point(2, 7)); - const lineC: LineSegment = lineSegment(point(1, 8), point(3, 8)); - const lineD: LineSegment = lineSegment(point(1, 8), point(3, 8)); - const lineE: LineSegment = lineSegment(point(1, 9), point(3, 9)); - const lineF: LineSegment = lineSegment(point(1, 2), point(3, 4)); - const lineG: LineSegment = lineSegment(point(0, 1), point(2, 3)); + const lineA: LineSegment = lineSegment( + pointFrom(1, 4), + pointFrom(3, 4), + ); + const lineB: LineSegment = lineSegment( + pointFrom(2, 1), + pointFrom(2, 7), + ); + const lineC: LineSegment = lineSegment( + pointFrom(1, 8), + pointFrom(3, 8), + ); + const lineD: LineSegment = lineSegment( + pointFrom(1, 8), + pointFrom(3, 8), + ); + const lineE: LineSegment = lineSegment( + pointFrom(1, 9), + pointFrom(3, 9), + ); + const lineF: LineSegment = lineSegment( + pointFrom(1, 2), + pointFrom(3, 4), + ); + const lineG: LineSegment = lineSegment( + pointFrom(0, 1), + pointFrom(2, 3), + ); it("intersection", () => { expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]); diff --git a/packages/utils/geometry/shape.ts b/packages/utils/geometry/shape.ts index f896f2e6f6..4670b23ab1 100644 --- a/packages/utils/geometry/shape.ts +++ b/packages/utils/geometry/shape.ts @@ -16,7 +16,7 @@ import type { Curve, LineSegment, Polygon, Radians } from "../../math"; import { curve, lineSegment, - point, + pointFrom, pointDistance, pointFromArray, pointFromVector, @@ -118,23 +118,23 @@ export const getPolygonShape = ( const cx = x + width / 2; const cy = y + height / 2; - const center: Point = point(cx, cy); + const center: Point = pointFrom(cx, cy); let data: Polygon; if (element.type === "diamond") { data = polygon( - pointRotateRads(point(cx, y), center, angle), - pointRotateRads(point(x + width, cy), center, angle), - pointRotateRads(point(cx, y + height), center, angle), - pointRotateRads(point(x, cy), center, angle), + pointRotateRads(pointFrom(cx, y), center, angle), + pointRotateRads(pointFrom(x + width, cy), center, angle), + pointRotateRads(pointFrom(cx, y + height), center, angle), + pointRotateRads(pointFrom(x, cy), center, angle), ); } else { data = polygon( - pointRotateRads(point(x, y), center, angle), - pointRotateRads(point(x + width, y), center, angle), - pointRotateRads(point(x + width, y + height), center, angle), - pointRotateRads(point(x, y + height), center, angle), + pointRotateRads(pointFrom(x, y), center, angle), + pointRotateRads(pointFrom(x + width, y), center, angle), + pointRotateRads(pointFrom(x + width, y + height), center, angle), + pointRotateRads(pointFrom(x, y + height), center, angle), ); } @@ -162,11 +162,11 @@ export const getSelectionBoxShape = ( y2 += padding; //const angleInDegrees = angleToDegrees(element.angle); - const center = point(cx, cy); - const topLeft = pointRotateRads(point(x1, y1), center, element.angle); - const topRight = pointRotateRads(point(x2, y1), center, element.angle); - const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle); - const bottomRight = pointRotateRads(point(x2, y2), center, element.angle); + const center = pointFrom(cx, cy); + const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle); + const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle); + const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle); + const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle); return { type: "polygon", @@ -183,7 +183,7 @@ export const getEllipseShape = ( return { type: "ellipse", data: { - center: point(x + width / 2, y + height / 2), + center: pointFrom(x + width / 2, y + height / 2), angle, halfWidth: width / 2, halfHeight: height / 2, @@ -203,20 +203,20 @@ export const getCurvePathOps = (shape: Drawable): Op[] => { // linear export const getCurveShape = ( roughShape: Drawable, - startingPoint: Point = point(0, 0), + startingPoint: Point = pointFrom(0, 0), angleInRadian: Radians, center: Point, ): GeometricShape => { const transform = (p: Point): Point => pointRotateRads( - point(p[0] + startingPoint[0], p[1] + startingPoint[1]), + pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]), center, angleInRadian, ); const ops = getCurvePathOps(roughShape); const polycurve: Polycurve = []; - let p0 = point(0, 0); + let p0 = pointFrom(0, 0); for (const op of ops) { if (op.op === "move") { @@ -225,9 +225,9 @@ export const getCurveShape = ( p0 = transform(p); } if (op.op === "bcurveTo") { - const p1 = transform(point(op.data[0], op.data[1])); - const p2 = transform(point(op.data[2], op.data[3])); - const p3 = transform(point(op.data[4], op.data[5])); + const p1 = transform(pointFrom(op.data[0], op.data[1])); + const p2 = transform(pointFrom(op.data[2], op.data[3])); + const p3 = transform(pointFrom(op.data[4], op.data[5])); polycurve.push(curve(p0, p1, p2, p3)); p0 = p3; } @@ -288,13 +288,13 @@ export const getFreedrawShape = ( export const getClosedCurveShape = ( element: ExcalidrawLinearElement, roughShape: Drawable, - startingPoint: Point = point(0, 0), + startingPoint: Point = pointFrom(0, 0), angleInRadian: Radians, center: Point, ): GeometricShape => { const transform = (p: Point) => pointRotateRads( - point(p[0] + startingPoint[0], p[1] + startingPoint[1]), + pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]), center, angleInRadian, ); @@ -316,17 +316,17 @@ export const getClosedCurveShape = ( if (operation.op === "move") { odd = !odd; if (odd) { - points.push(point(operation.data[0], operation.data[1])); + points.push(pointFrom(operation.data[0], operation.data[1])); } } else if (operation.op === "bcurveTo") { if (odd) { - points.push(point(operation.data[0], operation.data[1])); - points.push(point(operation.data[2], operation.data[3])); - points.push(point(operation.data[4], operation.data[5])); + points.push(pointFrom(operation.data[0], operation.data[1])); + points.push(pointFrom(operation.data[2], operation.data[3])); + points.push(pointFrom(operation.data[4], operation.data[5])); } } else if (operation.op === "lineTo") { if (odd) { - points.push(point(operation.data[0], operation.data[1])); + points.push(pointFrom(operation.data[0], operation.data[1])); } } } @@ -364,27 +364,27 @@ export const segmentIntersectRectangleElement = < element.x + element.width + gap, element.y + element.height + gap, ]; - const center = point( + const center = pointFrom( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, ); return [ lineSegment( - pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), - pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), + pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle), + pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle), ), lineSegment( - pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), - pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), + pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle), + pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle), ), lineSegment( - pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), - pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), + pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle), + pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle), ), lineSegment( - pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), - pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), + pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle), + pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle), ), ] .map((s) => segmentsIntersectAt(segment, s)) @@ -404,7 +404,7 @@ const distanceToEllipse = ( ); const [rotatedPointX, rotatedPointY] = pointRotateRads( pointFromVector(translatedPoint), - point(0, 0), + pointFrom(0, 0), -angle as Radians, ); @@ -442,7 +442,10 @@ const distanceToEllipse = ( b * ty * Math.sign(rotatedPointY), ]; - return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY)); + return pointDistance( + pointFrom(rotatedPointX, rotatedPointY), + pointFrom(minX, minY), + ); }; export const pointOnEllipse = ( @@ -464,7 +467,7 @@ export const pointInEllipse = ( ); const [rotatedPointX, rotatedPointY] = pointRotateRads( pointFromVector(translatedPoint), - point(0, 0), + pointFrom(0, 0), -angle as Radians, ); diff --git a/packages/utils/withinBounds.ts b/packages/utils/withinBounds.ts index 1920c15cdd..72c53aedfb 100644 --- a/packages/utils/withinBounds.ts +++ b/packages/utils/withinBounds.ts @@ -17,7 +17,7 @@ import { arrayToMap } from "../excalidraw/utils"; import type { LocalPoint } from "../math"; import { rangeIncludesValue, - point, + pointFrom, pointRotateRads, rangeInclusive, } from "../math"; @@ -41,17 +41,17 @@ const getNonLinearElementRelativePoints = ( ] => { if (element.type === "diamond") { return [ - point(element.width / 2, 0), - point(element.width, element.height / 2), - point(element.width / 2, element.height), - point(0, element.height / 2), + pointFrom(element.width / 2, 0), + pointFrom(element.width, element.height / 2), + pointFrom(element.width / 2, element.height), + pointFrom(0, element.height / 2), ]; } return [ - point(0, 0), - point(0 + element.width, 0), - point(0 + element.width, element.height), - point(0, element.height), + pointFrom(0, 0), + pointFrom(0 + element.width, 0), + pointFrom(0 + element.width, element.height), + pointFrom(0, element.height), ]; }; @@ -94,7 +94,7 @@ const getRotatedBBox = (element: Element): Bounds => { const points = getElementRelativePoints(element); const { cx, cy } = getMinMaxPoints(points); - const centerPoint = point(cx, cy); + const centerPoint = pointFrom(cx, cy); const rotatedPoints = points.map((p) => pointRotateRads(p, centerPoint, element.angle),