diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 83b0ad5290..ef149d6d86 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -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, roundToStep } from "../../math"; +import { clamp, point, roundToStep } from "../../math"; export const actionChangeViewBackgroundColor = register({ name: "changeViewBackgroundColor", @@ -324,7 +324,7 @@ export const zoomToFitBounds = ({ ); const centerScroll = centerScrollOn({ - scenePoint: { x: centerX, y: centerY }, + scenePoint: point(centerX, centerY), viewportDimensions: { width: appState.width, height: appState.height, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f8c80e52e8..c6cc142f3c 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -127,7 +127,7 @@ export const actionFinalize = register({ !isLoop && multiPointElement.points.length > 1 ) { - const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( + const p = LinearElementEditor.getPointAtIndexGlobalCoordinates( multiPointElement, -1, arrayToMap(elements), @@ -135,7 +135,7 @@ export const actionFinalize = register({ maybeBindLinearElement( multiPointElement, appState, - { x, y }, + p, elementsMap, elements, ); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 92fa329473..4026d47798 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -98,12 +98,7 @@ import { isSomeElementSelected, } from "../scene"; import { hasStrokeColor } from "../scene/comparisons"; -import { - arrayToMap, - getFontFamilyString, - getShortcutKey, - tupleToCoors, -} from "../utils"; +import { arrayToMap, getFontFamilyString, getShortcutKey } from "../utils"; import { register } from "./register"; import { StoreAction } from "../store"; import { Fonts, getLineHeight } from "../fonts"; @@ -1588,7 +1583,7 @@ export const actionChangeArrowType = register({ const startHoveredElement = !newElement.startBinding && getHoveredElementForBinding( - tupleToCoors(startGlobalPoint), + startGlobalPoint, elements, elementsMap, true, @@ -1596,7 +1591,7 @@ export const actionChangeArrowType = register({ const endHoveredElement = !newElement.endBinding && getHoveredElementForBinding( - tupleToCoors(endGlobalPoint), + endGlobalPoint, elements, elementsMap, true, diff --git a/packages/excalidraw/animated-trail.ts b/packages/excalidraw/animated-trail.ts index 97a0054614..0570c02e83 100644 --- a/packages/excalidraw/animated-trail.ts +++ b/packages/excalidraw/animated-trail.ts @@ -5,6 +5,7 @@ import type { AppState } from "./types"; import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils"; import type App from "./components/App"; import { SVG_NS } from "./constants"; +import { point } from "../math"; export interface Trail { start(container: SVGSVGElement): void; @@ -135,14 +136,7 @@ export class AnimatedTrail implements Trail { private drawTrail(trail: LaserPointer, state: AppState): string { const stroke = trail .getStrokeOutline(trail.options.size / state.zoom.value) - .map(([x, y]) => { - const result = sceneCoordsToViewportCoords( - { sceneX: x, sceneY: y }, - state, - ); - - return [result.x, result.y]; - }); + .map((p) => sceneCoordsToViewportCoords(point(p[0], p[1]), state)); return getSvgPathFromStroke(stroke, true); } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 08ad13fa54..71a4d05546 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -187,7 +187,6 @@ import type { ExcalidrawArrowElement, NonDeletedSceneElementsMap, } from "../element/types"; -import { getCenter, getDistance } from "../gesture"; import { editGroupForSelectedElement, getElementsInGroup, @@ -264,7 +263,6 @@ import type { } from "../types"; import { debounce, - distance, getFontString, getNearestScrollableContainer, isInputLike, @@ -444,8 +442,21 @@ import { getLinkDirectionFromKey, } from "../element/flowchart"; import { searchItemInFocusAtom } from "./SearchMenu"; -import type { LocalPoint, Radians } from "../../math"; -import { point, pointDistance, vector } from "../../math"; +import type { + GlobalPoint, + LocalPoint, + Radians, + ViewportPoint, +} from "../../math"; +import { + point, + pointCenter, + pointDistance, + pointSubtract, + rangeExtent, + rangeInclusive, + vector, +} from "../../math"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -589,7 +600,7 @@ class App extends React.Component { lastPointerUpEvent: React.PointerEvent | PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null; - lastViewportPosition = { x: 0, y: 0 }; + lastViewportPosition = point(0, 0); animationFrameHandler = new AnimationFrameHandler(); @@ -918,8 +929,7 @@ class App extends React.Component { private isIframeLikeElementCenter( el: ExcalidrawIframeLikeElement | null, event: React.PointerEvent | PointerEvent, - sceneX: number, - sceneY: number, + [sceneX, sceneY]: GlobalPoint, ) { return ( el && @@ -997,8 +1007,8 @@ class App extends React.Component { return ( <> {embeddableElements.map((el) => { - const { x, y } = sceneCoordsToViewportCoords( - { sceneX: el.x, sceneY: el.y }, + const [x, y] = sceneCoordsToViewportCoords( + point(el.x, el.y), this.state, ); @@ -1284,19 +1294,19 @@ class App extends React.Component { if (frameNameDiv) { const box = frameNameDiv.getBoundingClientRect(); const boxSceneTopLeft = viewportCoordsToSceneCoords( - { clientX: box.x, clientY: box.y }, + point(box.x, box.y), this.state, ); const boxSceneBottomRight = viewportCoordsToSceneCoords( - { clientX: box.right, clientY: box.bottom }, + point(box.right, box.bottom), this.state, ); bounds = { - x: boxSceneTopLeft.x, - y: boxSceneTopLeft.y, - width: boxSceneBottomRight.x - boxSceneTopLeft.x, - height: boxSceneBottomRight.y - boxSceneTopLeft.y, + x: boxSceneTopLeft[0], + y: boxSceneTopLeft[1], + width: boxSceneBottomRight[0] - boxSceneTopLeft[0], + height: boxSceneBottomRight[1] - boxSceneTopLeft[1], angle: 0, zoom: this.state.zoom.value, versionNonce: frameElement.versionNonce, @@ -1344,10 +1354,7 @@ class App extends React.Component { return null; } - const { x: x1, y: y1 } = sceneCoordsToViewportCoords( - { sceneX: f.x, sceneY: f.y }, - this.state, - ); + const [x1, y1] = sceneCoordsToViewportCoords(point(f.x, f.y), this.state); const FRAME_NAME_EDIT_PADDING = 6; @@ -2780,12 +2787,10 @@ class App extends React.Component { maybeBindLinearElement( multiElement, this.state, - tupleToCoors( - LinearElementEditor.getPointAtIndexGlobalCoordinates( - multiElement, - -1, - nonDeletedElementsMap, - ), + LinearElementEditor.getPointAtIndexGlobalCoordinates( + multiElement, + -1, + nonDeletedElementsMap, ), this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), @@ -2928,8 +2933,8 @@ class App extends React.Component { } const elementUnderCursor = document.elementFromPoint( - this.lastViewportPosition.x, - this.lastViewportPosition.y, + this.lastViewportPosition[0], + this.lastViewportPosition[1], ); if ( event && @@ -2939,11 +2944,8 @@ class App extends React.Component { return; } - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - { - clientX: this.lastViewportPosition.x, - clientY: this.lastViewportPosition.y, - }, + const [sceneX, sceneY] = viewportCoordsToSceneCoords( + this.lastViewportPosition, this.state, ); @@ -3152,24 +3154,24 @@ class App extends React.Component { elements = restoreElements(elements, null, undefined); const [minX, minY, maxX, maxY] = getCommonBounds(elements); - const elementsCenterX = distance(minX, maxX) / 2; - const elementsCenterY = distance(minY, maxY) / 2; + const elementsCenterX = rangeExtent(rangeInclusive(minX, maxX)) / 2; + const elementsCenterY = rangeExtent(rangeInclusive(minY, maxY)) / 2; const clientX = typeof opts.position === "object" ? opts.position.clientX : opts.position === "cursor" - ? this.lastViewportPosition.x + ? this.lastViewportPosition[0] : this.state.width / 2 + this.state.offsetLeft; const clientY = typeof opts.position === "object" ? opts.position.clientY : opts.position === "cursor" - ? this.lastViewportPosition.y + ? this.lastViewportPosition[1] : this.state.height / 2 + this.state.offsetTop; - const { x, y } = viewportCoordsToSceneCoords( - { clientX, clientY }, + const [x, y] = viewportCoordsToSceneCoords( + point(clientX, clientY), this.state, ); @@ -3195,7 +3197,7 @@ class App extends React.Component { syncMovedIndices(nextElements, arrayToMap(newElements)); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(point(x, y)); if (topLayerFrame) { const eligibleElements = filterElementsEligibleAsFrameChildren( @@ -3365,11 +3367,8 @@ class App extends React.Component { } private addTextFromPaste(text: string, isPlainPaste = false) { - const { x, y } = viewportCoordsToSceneCoords( - { - clientX: this.lastViewportPosition.x, - clientY: this.lastViewportPosition.y, - }, + const [x, y] = viewportCoordsToSceneCoords( + this.lastViewportPosition, this.state, ); @@ -3407,10 +3406,9 @@ class App extends React.Component { (acc: ExcalidrawTextElement[], line, idx) => { const originalText = normalizeText(line).trim(); if (originalText.length) { - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x, - y: currentY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords( + point(x, currentY), + ); let metrics = measureText(originalText, fontString, lineHeight); const isTextUnwrapped = metrics.width > maxTextWidth; @@ -3850,8 +3848,7 @@ class App extends React.Component { private updateCurrentCursorPosition = withBatchedUpdates( (event: MouseEvent) => { - this.lastViewportPosition.x = event.clientX; - this.lastViewportPosition.y = event.clientY; + this.lastViewportPosition = point(event.clientX, event.clientY); }, ); @@ -4242,11 +4239,8 @@ class App extends React.Component { this.state, this.scene.getNonDeletedElementsMap(), ); - const sceneX = midPoint.x; - const sceneY = midPoint.y; this.startTextEditing({ - sceneX, - sceneY, + sceneCoords: midPoint, container, }); event.preventDefault(); @@ -4622,8 +4616,8 @@ class App extends React.Component { this.setState((state) => ({ ...getStateForZoom( { - viewportX: this.lastViewportPosition.x, - viewportY: this.lastViewportPosition.y, + viewportX: this.lastViewportPosition[0], + viewportY: this.lastViewportPosition[1], nextZoom: getNormalizedZoom(initialScale * event.scale), }, state, @@ -4684,11 +4678,8 @@ class App extends React.Component { id: element.id, canvas: this.canvas, getViewportCoords: (x, y) => { - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( - { - sceneX: x, - sceneY: y, - }, + const [viewportX, viewportY] = sceneCoordsToViewportCoords( + point(x, y), this.state, ); return [ @@ -4774,10 +4765,9 @@ class App extends React.Component { } private getTextElementAtPosition( - x: number, - y: number, + position: GlobalPoint, ): NonDeleted | null { - const element = this.getElementAtPosition(x, y, { + const element = this.getElementAtPosition(position, { includeBoundTextElement: true, }); if (element && isTextElement(element) && !element.isDeleted) { @@ -4787,8 +4777,7 @@ class App extends React.Component { } private getElementAtPosition( - x: number, - y: number, + sceneCoords: GlobalPoint, opts?: { preferSelected?: boolean; includeBoundTextElement?: boolean; @@ -4796,8 +4785,7 @@ class App extends React.Component { }, ): NonDeleted | null { const allHitElements = this.getElementsAtPosition( - x, - y, + sceneCoords, opts?.includeBoundTextElement, opts?.includeLockedElements, ); @@ -4816,8 +4804,7 @@ class App extends React.Component { // If we're hitting element with highest z-index only on its bounding box // while also hitting other element figure, the latter should be considered. return hitElementItself({ - x, - y, + sceneCoords, element: elementWithHighestZIndex, shape: getElementShape( elementWithHighestZIndex, @@ -4841,8 +4828,7 @@ class App extends React.Component { } private getElementsAtPosition( - x: number, - y: number, + scenePointer: GlobalPoint, includeBoundTextElement: boolean = false, includeLockedElements: boolean = false, ): NonDeleted[] { @@ -4862,14 +4848,14 @@ class App extends React.Component { !(isTextElement(element) && element.containerId)), ) ) - .filter((el) => this.hitElement(x, y, el)) + .filter((el) => this.hitElement(scenePointer, el)) .filter((element) => { // hitting a frame's element from outside the frame is not considered a hit const containingFrame = getContainingFrame(element, elementsMap); return containingFrame && this.state.frameRendering.enabled && this.state.frameRendering.clip - ? isCursorInFrame({ x, y }, containingFrame, elementsMap) + ? isCursorInFrame(scenePointer, containingFrame, elementsMap) : true; }) .filter((el) => { @@ -4893,8 +4879,7 @@ class App extends React.Component { } private hitElement( - x: number, - y: number, + sceneCoords: GlobalPoint, element: ExcalidrawElement, considerBoundingBox = true, ) { @@ -4910,13 +4895,12 @@ class App extends React.Component { this.getElementHitThreshold(), ); - return isPointInShape(point(x, y), selectionShape); + return isPointInShape(sceneCoords, selectionShape); } // take bound text element into consideration for hit collision as well const hitBoundTextOfElement = hitElementBoundText( - x, - y, + sceneCoords, getBoundTextShape(element, this.scene.getNonDeletedElementsMap()), ); if (hitBoundTextOfElement) { @@ -4924,8 +4908,7 @@ class App extends React.Component { } return hitElementItself({ - x, - y, + sceneCoords, element, shape: getElementShape(element, this.scene.getNonDeletedElementsMap()), threshold: this.getElementHitThreshold(), @@ -4935,7 +4918,7 @@ class App extends React.Component { }); } - private getTextBindableContainerAtPosition(x: number, y: number) { + private getTextBindableContainerAtPosition(position: GlobalPoint) { const elements = this.scene.getNonDeletedElements(); const selectedElements = this.scene.getSelectedElements(this.state); if (selectedElements.length === 1) { @@ -4956,8 +4939,7 @@ class App extends React.Component { if ( isArrowElement(elements[index]) && hitElementItself({ - x, - y, + sceneCoords: position, element: elements[index], shape: getElementShape( elements[index], @@ -4968,7 +4950,12 @@ class App extends React.Component { ) { hitElement = elements[index]; break; - } else if (x1 < x && x < x2 && y1 < y && y < y2) { + } else if ( + x1 < position[0] && + position[0] < x2 && + y1 < position[1] && + position[1] < y2 + ) { hitElement = elements[index]; break; } @@ -4978,16 +4965,13 @@ class App extends React.Component { } private startTextEditing = ({ - sceneX, - sceneY, + sceneCoords, insertAtParentCenter = true, container, autoEdit = true, }: { - /** X position to insert text at */ - sceneX: number; - /** Y position to insert text at */ - sceneY: number; + /** The position to insert text at */ + sceneCoords: GlobalPoint; /** whether to attempt to insert at element center if applicable */ insertAtParentCenter?: boolean; container?: ExcalidrawTextContainer | null; @@ -4998,8 +4982,7 @@ class App extends React.Component { let parentCenterPosition = insertAtParentCenter && this.getTextWysiwygSnappedToCenterPosition( - sceneX, - sceneY, + sceneCoords, this.state, container, ); @@ -5025,10 +5008,10 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); } else { - existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); + existingTextElement = this.getTextElementAtPosition(sceneCoords); } } else { - existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); + existingTextElement = this.getTextElementAtPosition(sceneCoords); } const fontFamily = @@ -5056,32 +5039,30 @@ class App extends React.Component { const newHeight = Math.max(container.height, minHeight); const newWidth = Math.max(container.width, minWidth); mutateElement(container, { height: newHeight, width: newWidth }); - sceneX = container.x + newWidth / 2; - sceneY = container.y + newHeight / 2; + sceneCoords = point( + container.x + newWidth / 2, + container.y + newHeight / 2, + ); if (parentCenterPosition) { parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition( - sceneX, - sceneY, + sceneCoords, this.state, container, ); } } - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: sceneX, - y: sceneY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(sceneCoords); const element = existingTextElement ? existingTextElement : newTextElement({ x: parentCenterPosition ? parentCenterPosition.elementCenterX - : sceneX, + : sceneCoords[0], y: parentCenterPosition ? parentCenterPosition.elementCenterY - : sceneY, + : sceneCoords[1], strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, @@ -5169,15 +5150,15 @@ class App extends React.Component { resetCursor(this.interactiveCanvas); - let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - event, + let sceneCoords = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), this.state, ); const selectedGroupIds = getSelectedGroupIds(this.state); if (selectedGroupIds.length > 0) { - const hitElement = this.getElementAtPosition(sceneX, sceneY); + const hitElement = this.getElementAtPosition(sceneCoords); const selectedGroupId = hitElement && @@ -5203,7 +5184,7 @@ class App extends React.Component { resetCursor(this.interactiveCanvas); if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) { - const hitElement = this.getElementAtPosition(sceneX, sceneY); + const hitElement = this.getElementAtPosition(sceneCoords); if (isIframeLikeElement(hitElement)) { this.setState({ @@ -5212,15 +5193,14 @@ class App extends React.Component { return; } - const container = this.getTextBindableContainerAtPosition(sceneX, sceneY); + const container = this.getTextBindableContainerAtPosition(sceneCoords); if (container) { if ( hasBoundTextElement(container) || !isTransparent(container.backgroundColor) || hitElementItself({ - x: sceneX, - y: sceneY, + sceneCoords, element: container, shape: getElementShape( container, @@ -5235,14 +5215,12 @@ class App extends React.Component { this.scene.getNonDeletedElementsMap(), ); - sceneX = midPoint.x; - sceneY = midPoint.y; + sceneCoords = midPoint; } } this.startTextEditing({ - sceneX, - sceneY, + sceneCoords, insertAtParentCenter: !event.altKey, container, }); @@ -5250,7 +5228,7 @@ class App extends React.Component { }; private getElementLinkAtPosition = ( - scenePointer: Readonly<{ x: number; y: number }>, + scenePointer: GlobalPoint, hitElement: NonDeletedExcalidrawElement | null, ): ExcalidrawElement | undefined => { // Reversing so we traverse the elements in decreasing order @@ -5269,7 +5247,7 @@ class App extends React.Component { element, this.scene.getNonDeletedElementsMap(), this.state, - point(scenePointer.x, scenePointer.y), + scenePointer, this.device.editor.isMobile, ) ); @@ -5296,7 +5274,10 @@ class App extends React.Component { return; } const lastPointerDownCoords = viewportCoordsToSceneCoords( - this.lastPointerDownEvent!, + point( + this.lastPointerDownEvent!.clientX, + this.lastPointerDownEvent!.clientY, + ), this.state, ); const elementsMap = this.scene.getNonDeletedElementsMap(); @@ -5304,18 +5285,18 @@ class App extends React.Component { this.hitLinkElement, elementsMap, this.state, - point(lastPointerDownCoords.x, lastPointerDownCoords.y), + lastPointerDownCoords, this.device.editor.isMobile, ); const lastPointerUpCoords = viewportCoordsToSceneCoords( - this.lastPointerUpEvent!, + point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY), this.state, ); const lastPointerUpHittingLinkIcon = isPointHittingLink( this.hitLinkElement, elementsMap, this.state, - point(lastPointerUpCoords.x, lastPointerUpCoords.y), + lastPointerUpCoords, this.device.editor.isMobile, ); if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { @@ -5346,10 +5327,7 @@ class App extends React.Component { } }; - private getTopLayerFrameAtSceneCoords = (sceneCoords: { - x: number; - y: number; - }) => { + private getTopLayerFrameAtSceneCoords = (sceneCoords: GlobalPoint) => { const elementsMap = this.scene.getNonDeletedElementsMap(); const frames = this.scene .getNonDeletedFramesLikes() @@ -5367,10 +5345,10 @@ class App extends React.Component { this.lastPointerMoveEvent = event.nativeEvent; if (gesture.pointers.has(event.pointerId)) { - gesture.pointers.set(event.pointerId, { - x: event.clientX, - y: event.clientY, - }); + gesture.pointers.set( + event.pointerId, + point(event.clientX, event.clientY), + ); } const initialScale = gesture.initialScale; @@ -5380,12 +5358,11 @@ class App extends React.Component { initialScale && gesture.initialDistance ) { - const center = getCenter(gesture.pointers); - const deltaX = center.x - gesture.lastCenter.x; - const deltaY = center.y - gesture.lastCenter.y; + const [pointer1, pointer2] = Array.from(gesture.pointers.values()); + const center = pointCenter(pointer1, pointer2); + const [deltaX, deltaY] = pointSubtract(center, gesture.lastCenter); gesture.lastCenter = center; - - const distance = getDistance(Array.from(gesture.pointers.values())); + const distance = pointDistance(pointer1, pointer2); const scaleFactor = this.state.activeTool.type === "freedraw" && this.state.penMode ? 1 @@ -5398,8 +5375,8 @@ class App extends React.Component { this.setState((state) => { const zoomState = getStateForZoom( { - viewportX: center.x, - viewportY: center.y, + viewportX: center[0], + viewportY: center[1], nextZoom, }, state, @@ -5451,8 +5428,10 @@ class App extends React.Component { } } - const scenePointer = viewportCoordsToSceneCoords(event, this.state); - const { x: scenePointerX, y: scenePointerY } = scenePointer; + const scenePointer = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), + this.state, + ); if ( !this.state.newElement && @@ -5462,8 +5441,8 @@ class App extends React.Component { this.scene.getNonDeletedElements(), this, { - x: scenePointerX, - y: scenePointerY, + x: scenePointer[0], + y: scenePointer[1], }, event, this.scene.getNonDeletedElementsMap(), @@ -5507,8 +5486,7 @@ class App extends React.Component { ) { const editingLinearElement = LinearElementEditor.handlePointerMove( event, - scenePointerX, - scenePointerY, + scenePointer, this, this.scene.getNonDeletedElementsMap(), ); @@ -5565,7 +5543,7 @@ class App extends React.Component { // threshold, add a point if ( pointDistance( - point(scenePointerX - rx, scenePointerY - ry), + point(scenePointer[0] - rx, scenePointer[1] - ry), lastPoint, ) >= LINE_CONFIRM_THRESHOLD ) { @@ -5574,7 +5552,7 @@ class App extends React.Component { { points: [ ...points, - point(scenePointerX - rx, scenePointerY - ry), + point(scenePointer[0] - rx, scenePointer[1] - ry), ], }, false, @@ -5588,7 +5566,7 @@ class App extends React.Component { points.length > 2 && lastCommittedPoint && pointDistance( - point(scenePointerX - rx, scenePointerY - ry), + point(scenePointer[0] - rx, scenePointer[1] - ry), lastCommittedPoint, ) < LINE_CONFIRM_THRESHOLD ) { @@ -5602,8 +5580,8 @@ class App extends React.Component { ); } else { const [gridX, gridY] = getGridPoint( - scenePointerX, - scenePointerY, + scenePointer[0], + scenePointer[1], event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) ? null : this.getEffectiveGridSize(), @@ -5697,8 +5675,7 @@ class App extends React.Component { if (this.state.selectedLinearElement) { this.handleHoverSelectedLinearElement( this.state.selectedLinearElement, - scenePointerX, - scenePointerY, + scenePointer, ); } @@ -5711,8 +5688,7 @@ class App extends React.Component { getElementWithTransformHandleType( elements, this.state, - scenePointerX, - scenePointerY, + scenePointer, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap(), @@ -5732,8 +5708,7 @@ class App extends React.Component { } else if (selectedElements.length > 1 && !isOverScrollBar) { const transformHandleType = getTransformHandleTypeFromCoords( getCommonBounds(selectedElements), - scenePointerX, - scenePointerY, + scenePointer, this.state.zoom, event.pointerType, this.device, @@ -5749,10 +5724,7 @@ class App extends React.Component { } } - const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, - ); + const hitElement = this.getElementAtPosition(scenePointer); this.hitLinkElement = this.getElementLinkAtPosition( scenePointer, @@ -5793,8 +5765,7 @@ class App extends React.Component { } else if (this.state.selectedLinearElement) { this.handleHoverSelectedLinearElement( this.state.selectedLinearElement, - scenePointerX, - scenePointerY, + scenePointer, ); } else if ( // if using cmd/ctrl, we're not dragging @@ -5811,12 +5782,7 @@ class App extends React.Component { if ( hitElement && isIframeLikeElement(hitElement) && - this.isIframeLikeElementCenter( - hitElement, - event, - scenePointerX, - scenePointerY, - ) + this.isIframeLikeElementCenter(hitElement, event, scenePointer) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); this.setState({ @@ -5838,9 +5804,9 @@ class App extends React.Component { private handleEraser = ( event: PointerEvent, pointerDownState: PointerDownState, - scenePointer: { x: number; y: number }, + scenePointer: GlobalPoint, ) => { - this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y); + this.eraserTrail.addPointToPath(scenePointer[0], scenePointer[1]); let didChange = false; @@ -5883,15 +5849,12 @@ class App extends React.Component { } }; - const distance = pointDistance( - point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y), - point(scenePointer.x, scenePointer.y), - ); + const distance = pointDistance(pointerDownState.lastCoords, scenePointer); const threshold = this.getElementHitThreshold(); - const p = { ...pointerDownState.lastCoords }; + let p = pointerDownState.lastCoords; let samplingInterval = 0; while (samplingInterval <= distance) { - const hitElements = this.getElementsAtPosition(p.x, p.y); + const hitElements = this.getElementsAtPosition(p); processElements(hitElements); // Exit since we reached current point @@ -5903,14 +5866,14 @@ class App extends React.Component { samplingInterval = Math.min(samplingInterval + threshold, distance); const distanceRatio = samplingInterval / distance; - const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x; - const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y; - p.x = nextX; - p.y = nextY; + const nextX = + (1 - distanceRatio) * p[0] + distanceRatio * scenePointer[0]; + const nextY = + (1 - distanceRatio) * p[1] + distanceRatio * scenePointer[1]; + p = point(nextX, nextY); } - pointerDownState.lastCoords.x = scenePointer.x; - pointerDownState.lastCoords.y = scenePointer.y; + pointerDownState.lastCoords = point(scenePointer[0], scenePointer[1]); if (didChange) { for (const element of this.scene.getNonDeletedElements()) { @@ -5941,8 +5904,7 @@ class App extends React.Component { handleHoverSelectedLinearElement( linearElementEditor: LinearElementEditor, - scenePointerX: number, - scenePointerY: number, + sceneCoords: GlobalPoint, ) { const elementsMap = this.scene.getNonDeletedElementsMap(); @@ -5959,8 +5921,7 @@ class App extends React.Component { let segmentMidPointHoveredCoords = null; if ( hitElementItself({ - x: scenePointerX, - y: scenePointerY, + sceneCoords, element, shape: getElementShape( element, @@ -5972,23 +5933,22 @@ class App extends React.Component { element, elementsMap, this.state.zoom, - scenePointerX, - scenePointerY, + sceneCoords, ); segmentMidPointHoveredCoords = LinearElementEditor.getSegmentMidpointHitCoords( linearElementEditor, - { x: scenePointerX, y: scenePointerY }, + sceneCoords, this.state, this.scene.getNonDeletedElementsMap(), ); if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - } else if (this.hitElement(scenePointerX, scenePointerY, element)) { + } else if (this.hitElement(sceneCoords, element)) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); } - } else if (this.hitElement(scenePointerX, scenePointerY, element)) { + } else if (this.hitElement(sceneCoords, element)) { if ( !isElbowArrow(element) || !(element.startBinding || element.endBinding) @@ -6264,13 +6224,16 @@ class App extends React.Component { multiElement: null, }); - const { x, y } = viewportCoordsToSceneCoords(event, this.state); + const sceneCoords = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), + this.state, + ); - const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); + const frame = this.getTopLayerFrameAtSceneCoords(sceneCoords); mutateElement(pendingImageElement, { - x, - y, + x: sceneCoords[0], + y: sceneCoords[1], frameId: frame ? frame.id : null, }); } else if (this.state.activeTool.type === "freedraw") { @@ -6291,8 +6254,8 @@ class App extends React.Component { ); } else if (this.state.activeTool.type === "laser") { this.laserTrails.startPath( - pointerDownState.lastCoords.x, - pointerDownState.lastCoords.y, + pointerDownState.lastCoords[0], + pointerDownState.lastCoords[1], ); } else if ( this.state.activeTool.type !== "eraser" && @@ -6313,8 +6276,8 @@ class App extends React.Component { if (this.state.activeTool.type === "eraser") { this.eraserTrail.startPath( - pointerDownState.lastCoords.x, - pointerDownState.lastCoords.y, + pointerDownState.lastCoords[0], + pointerDownState.lastCoords[1], ); } @@ -6350,25 +6313,17 @@ class App extends React.Component { this.lastPointerUpEvent = event; const scenePointer = viewportCoordsToSceneCoords( - { clientX: event.clientX, clientY: event.clientY }, + point(event.clientX, event.clientY), this.state, ); const clicklength = event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); if (this.device.editor.isMobile && clicklength < 300) { - const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, - ); + const hitElement = this.getElementAtPosition(scenePointer); if ( isIframeLikeElement(hitElement) && - this.isIframeLikeElementCenter( - hitElement, - event, - scenePointer.x, - scenePointer.y, - ) + this.isIframeLikeElementCenter(hitElement, event, scenePointer) ) { this.handleEmbeddableCenterClick(hitElement); return; @@ -6376,10 +6331,7 @@ class App extends React.Component { } if (this.device.isTouchScreen) { - const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, - ); + const hitElement = this.getElementAtPosition(scenePointer); this.hitLinkElement = this.getElementLinkAtPosition( scenePointer, hitElement, @@ -6397,7 +6349,7 @@ class App extends React.Component { this.hitLinkElement, this.scene.getNonDeletedElementsMap(), this.state, - point(scenePointer.x, scenePointer.y), + scenePointer, ) ) { this.handleEmbeddableCenterClick(this.hitLinkElement); @@ -6568,24 +6520,23 @@ class App extends React.Component { private updateGestureOnPointerDown( event: React.PointerEvent, ): void { - gesture.pointers.set(event.pointerId, { - x: event.clientX, - y: event.clientY, - }); + gesture.pointers.set(event.pointerId, point(event.clientX, event.clientY)); if (gesture.pointers.size === 2) { - gesture.lastCenter = getCenter(gesture.pointers); + const [pointer1, pointer2] = Array.from(gesture.pointers.values()); + gesture.lastCenter = pointCenter(pointer1, pointer2); gesture.initialScale = this.state.zoom.value; - gesture.initialDistance = getDistance( - Array.from(gesture.pointers.values()), - ); + gesture.initialDistance = pointDistance(pointer1, pointer2); } } private initialPointerDownState( event: React.PointerEvent, ): PointerDownState { - const origin = viewportCoordsToSceneCoords(event, this.state); + const origin = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), + this.state, + ); const selectedElements = this.scene.getSelectedElements(this.state); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow) === 0; @@ -6595,8 +6546,8 @@ class App extends React.Component { withCmdOrCtrl: event[KEYS.CTRL_OR_CMD], originInGrid: tupleToCoors( getGridPoint( - origin.x, - origin.y, + origin[0], + origin[1], event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly ? null : this.getEffectiveGridSize(), @@ -6608,7 +6559,7 @@ class App extends React.Component { event.clientY - this.state.offsetTop, ), // we need to duplicate because we'll be updating this state - lastCoords: { ...origin }, + lastCoords: origin, originalElements: this.scene .getNonDeletedElements() .reduce((acc, element) => { @@ -6660,8 +6611,7 @@ class App extends React.Component { return false; } isDraggingScrollBar = true; - pointerDownState.lastCoords.x = event.clientX; - pointerDownState.lastCoords.y = event.clientY; + pointerDownState.lastCoords = point(event.clientX, event.clientY); const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => { const target = event.target; if (!(target instanceof HTMLElement)) { @@ -6725,8 +6675,7 @@ class App extends React.Component { getElementWithTransformHandleType( elements, this.state, - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap(), @@ -6742,8 +6691,7 @@ class App extends React.Component { } else if (selectedElements.length > 1) { pointerDownState.resize.handleType = getTransformHandleTypeFromCoords( getCommonBounds(selectedElements), - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin, this.state.zoom, event.pointerType, this.device, @@ -6756,8 +6704,7 @@ class App extends React.Component { pointerDownState.resize.handleType, selectedElements, elementsMap, - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin, ), ); if ( @@ -6799,18 +6746,12 @@ class App extends React.Component { // hitElement may already be set above, so check first pointerDownState.hit.element = pointerDownState.hit.element ?? - this.getElementAtPosition( - pointerDownState.origin.x, - pointerDownState.origin.y, - ); + this.getElementAtPosition(pointerDownState.origin); if (pointerDownState.hit.element) { // Early return if pointer is hitting link icon const hitLinkElement = this.getElementLinkAtPosition( - { - x: pointerDownState.origin.x, - y: pointerDownState.origin.y, - }, + pointerDownState.origin, pointerDownState.hit.element, ); if (hitLinkElement) { @@ -6821,8 +6762,7 @@ class App extends React.Component { // For overlapped elements one position may hit // multiple elements pointerDownState.hit.allHitElements = this.getElementsAtPosition( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin, ); const hitElement = pointerDownState.hit.element; @@ -6995,7 +6935,7 @@ class App extends React.Component { } private isHittingCommonBoundingBoxOfSelectedElements( - point: Readonly<{ x: number; y: number }>, + point: GlobalPoint, selectedElements: readonly ExcalidrawElement[], ): boolean { if (selectedElements.length < 2) { @@ -7006,10 +6946,10 @@ class App extends React.Component { const threshold = this.getElementHitThreshold(); const [x1, y1, x2, y2] = getCommonBounds(selectedElements); return ( - point.x > x1 - threshold && - point.x < x2 + threshold && - point.y > y1 - threshold && - point.y < y2 + threshold + point[0] > x1 - threshold && + point[0] < x2 + threshold && + point[1] > y1 - threshold && + point[1] < y2 + threshold ); } @@ -7023,24 +6963,25 @@ class App extends React.Component { if (this.state.editingTextElement) { return; } - let sceneX = pointerDownState.origin.x; - let sceneY = pointerDownState.origin.y; - const element = this.getElementAtPosition(sceneX, sceneY, { + let sceneCoords = pointerDownState.origin; + + const element = this.getElementAtPosition(sceneCoords, { includeBoundTextElement: true, }); // FIXME - let container = this.getTextBindableContainerAtPosition(sceneX, sceneY); + let container = this.getTextBindableContainerAtPosition(sceneCoords); if (hasBoundTextElement(element)) { container = element as ExcalidrawTextContainer; - sceneX = element.x + element.width / 2; - sceneY = element.y + element.height / 2; + sceneCoords = point( + element.x + element.width / 2, + element.y + element.height / 2, + ); } this.startTextEditing({ - sceneX, - sceneY, + sceneCoords, insertAtParentCenter: !event.altKey, container, autoEdit: false, @@ -7061,15 +7002,14 @@ class App extends React.Component { ) => { // Begin a mark capture. This does not have to update state yet. const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin[0], + pointerDownState.origin[1], null, ); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords( + point(gridX, gridY), + ); const simulatePressure = event.pressure === 0.5; @@ -7233,10 +7173,7 @@ class App extends React.Component { ); const topLayerFrame = addToFrameUnderCursor - ? this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }) + ? this.getTopLayerFrameAtSceneCoords(point(gridX, gridY)) : null; const element = newImageElement({ @@ -7297,7 +7234,10 @@ class App extends React.Component { multiElement.points.length > 1 && lastCommittedPoint && pointDistance( - point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry), + point( + pointerDownState.origin[0] - rx, + pointerDownState.origin[1] - ry, + ), lastCommittedPoint, ) < LINE_CONFIRM_THRESHOLD ) { @@ -7322,15 +7262,14 @@ class App extends React.Component { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } else { const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin[0], + pointerDownState.origin[1], event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords( + point(gridX, gridY), + ); /* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads. If so, we want it to be null for start and "arrow" for end. If the linear item is not @@ -7440,17 +7379,16 @@ class App extends React.Component { pointerDownState: PointerDownState, ): void => { const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin[0], + pointerDownState.origin[1], this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords( + point(gridX, gridY), + ); const baseElementAttributes = { x: gridX, @@ -7498,8 +7436,8 @@ class App extends React.Component { type: Extract, ): void => { const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin[0], + pointerDownState.origin[1], this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), @@ -7610,8 +7548,8 @@ class App extends React.Component { pointerDownState.drag.offset = tupleToCoors( getDragOffsetXY( this.scene.getSelectedElements(this.state), - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin[0], + pointerDownState.origin[1], ), ); } @@ -7624,7 +7562,10 @@ class App extends React.Component { return; } - const pointerCoords = viewportCoordsToSceneCoords(event, this.state); + const pointerCoords = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), + this.state, + ); if (isEraserActive(this.state)) { this.handleEraser(event, pointerDownState, pointerCoords); @@ -7632,12 +7573,12 @@ class App extends React.Component { } if (this.state.activeTool.type === "laser") { - this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); + this.laserTrails.addPointToPath(pointerCoords[0], pointerCoords[1]); } const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoords[0], + pointerCoords[1], event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -7651,17 +7592,14 @@ class App extends React.Component { this.state.activeTool.type === "line") ) { if ( - pointDistance( - point(pointerCoords.x, pointerCoords.y), - point(pointerDownState.origin.x, pointerDownState.origin.y), - ) < DRAGGING_THRESHOLD + pointDistance(pointerCoords, pointerDownState.origin) < + DRAGGING_THRESHOLD ) { return; } } if (pointerDownState.resize.isResizing) { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + pointerDownState.lastCoords = pointerCoords; if (this.maybeHandleResize(pointerDownState, event)) { return true; } @@ -7727,8 +7665,7 @@ class App extends React.Component { const didDrag = LinearElementEditor.handlePointDragging( event, this, - pointerCoords.x, - pointerCoords.y, + pointerCoords, (element, pointsSceneCoords) => { this.maybeSuggestBindingsForLinearElementAtCoords( element, @@ -7739,8 +7676,7 @@ class App extends React.Component { this.scene, ); if (didDrag) { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + pointerDownState.lastCoords = pointerCoords; pointerDownState.drag.hasOccurred = true; if ( this.state.editingLinearElement && @@ -7808,8 +7744,8 @@ class App extends React.Component { this.state.activeEmbeddable?.state !== "active" ) { const dragOffset = { - x: pointerCoords.x - pointerDownState.origin.x, - y: pointerCoords.y - pointerDownState.origin.y, + x: pointerCoords[0] - pointerDownState.origin[0], + y: pointerCoords[1] - pointerDownState.origin[1], }; const originalElements = [ @@ -7973,8 +7909,7 @@ class App extends React.Component { } if (this.state.selectionElement) { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + pointerDownState.lastCoords = pointerCoords; this.maybeDragNewGenericElement(pointerDownState, event); } else { // It is very important to read this.state within each move event, @@ -7987,8 +7922,8 @@ class App extends React.Component { if (newElement.type === "freedraw") { const points = newElement.points; - const dx = pointerCoords.x - newElement.x; - const dy = pointerCoords.y - newElement.y; + const dx = pointerCoords[0] - newElement.x; + const dy = pointerCoords[1] - newElement.y; const lastPoint = points.length > 0 && points[points.length - 1]; const discardPoint = @@ -8022,8 +7957,8 @@ class App extends React.Component { ({ width: dx, height: dy } = getLockedLinearCursorAlignSize( newElement.x, newElement.y, - pointerCoords.x, - pointerCoords.y, + pointerCoords[0], + pointerCoords[1], )); } @@ -8070,8 +8005,7 @@ class App extends React.Component { ); } } else { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + pointerDownState.lastCoords = pointerCoords; this.maybeDragNewGenericElement(pointerDownState, event, false); } } @@ -8185,21 +8119,21 @@ class App extends React.Component { ): boolean { if (pointerDownState.scrollbars.isOverHorizontal) { const x = event.clientX; - const dx = x - pointerDownState.lastCoords.x; + const dx = x - pointerDownState.lastCoords[0]; this.translateCanvas({ scrollX: this.state.scrollX - dx / this.state.zoom.value, }); - pointerDownState.lastCoords.x = x; + pointerDownState.lastCoords = point(x, pointerDownState.lastCoords[1]); return true; } if (pointerDownState.scrollbars.isOverVertical) { const y = event.clientY; - const dy = y - pointerDownState.lastCoords.y; + const dy = y - pointerDownState.lastCoords[1]; this.translateCanvas({ scrollY: this.state.scrollY - dy / this.state.zoom.value, }); - pointerDownState.lastCoords.y = y; + pointerDownState.lastCoords = point(pointerDownState.lastCoords[0], y); return true; } return false; @@ -8341,13 +8275,13 @@ class App extends React.Component { if (newElement?.type === "freedraw") { const pointerCoords = viewportCoordsToSceneCoords( - childEvent, + point(childEvent.clientX, childEvent.clientY), this.state, ); const points = newElement.points; - let dx = pointerCoords.x - newElement.x; - let dy = pointerCoords.y - newElement.y; + let dx = pointerCoords[0] - newElement.x; + let dy = pointerCoords[1] - newElement.y; // Allows dots to avoid being flagged as infinitely small if (dx === points[0][0] && dy === points[0][1]) { @@ -8401,7 +8335,7 @@ class App extends React.Component { this.store.shouldCaptureIncrement(); } const pointerCoords = viewportCoordsToSceneCoords( - childEvent, + point(childEvent.clientX, childEvent.clientY), this.state, ); @@ -8410,8 +8344,8 @@ class App extends React.Component { points: [ ...newElement.points, point( - pointerCoords.x - newElement.x, - pointerCoords.y - newElement.y, + pointerCoords[0] - newElement.x, + pointerCoords[1] - newElement.y, ), ], }); @@ -8525,7 +8459,10 @@ class App extends React.Component { } if (pointerDownState.drag.hasOccurred) { - const sceneCoords = viewportCoordsToSceneCoords(childEvent, this.state); + const sceneCoords = viewportCoordsToSceneCoords( + point(childEvent.clientX, childEvent.clientY), + this.state, + ); // when editing the points of a linear element, we check if the // linear element still is in the frame afterwards @@ -8729,16 +8666,10 @@ class App extends React.Component { if (draggedDistance === 0) { const scenePointer = viewportCoordsToSceneCoords( - { - clientX: pointerEnd.clientX, - clientY: pointerEnd.clientY, - }, + point(pointerEnd.clientX, pointerEnd.clientY), this.state, ); - const hitElements = this.getElementsAtPosition( - scenePointer.x, - scenePointer.y, - ); + const hitElements = this.getElementsAtPosition(scenePointer); hitElements.forEach((hitElement) => this.elementsPendingErasure.add(hitElement.id), ); @@ -8911,8 +8842,7 @@ class App extends React.Component { ((hitElement && hitElementBoundingBoxOnly( { - x: pointerDownState.origin.x, - y: pointerDownState.origin.y, + sceneCoords: pointerDownState.origin, element: hitElement, shape: getElementShape( hitElement, @@ -9019,8 +8949,7 @@ class App extends React.Component { this.isIframeLikeElementCenter( hitElement, this.lastPointerUpEvent, - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownState.origin, ) ) { this.handleEmbeddableCenterClick(hitElement); @@ -9273,8 +9202,8 @@ class App extends React.Component { const clientX = this.state.width / 2 + this.state.offsetLeft; const clientY = this.state.height / 2 + this.state.offsetTop; - const { x, y } = viewportCoordsToSceneCoords( - { clientX, clientY }, + const [x, y] = viewportCoordsToSceneCoords( + point(clientX, clientY), this.state, ); @@ -9464,10 +9393,7 @@ class App extends React.Component { } }; - private maybeSuggestBindingAtCursor = (pointerCoords: { - x: number; - y: number; - }): void => { + private maybeSuggestBindingAtCursor = (pointerCoords: GlobalPoint): void => { const hoveredBindableElement = getHoveredElementForBinding( pointerCoords, this.scene.getNonDeletedElements(), @@ -9482,10 +9408,7 @@ class App extends React.Component { private maybeSuggestBindingsForLinearElementAtCoords = ( linearElement: NonDeleted, /** scene coords */ - pointerCoords: { - x: number; - y: number; - }[], + pointerCoords: GlobalPoint[], // During line creation the start binding hasn't been written yet // into `linearElement` oppositeBindingBoundElement?: ExcalidrawBindableElement | null, @@ -9571,8 +9494,8 @@ class App extends React.Component { private handleAppOnDrop = async (event: React.DragEvent) => { // must be retrieved first, in the same frame const { file, fileHandle } = await getFileFromEvent(event); - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - event, + const [sceneX, sceneY] = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), this.state, ); @@ -9764,8 +9687,11 @@ class App extends React.Component { return; } - const { x, y } = viewportCoordsToSceneCoords(event, this.state); - const element = this.getElementAtPosition(x, y, { + const position = viewportCoordsToSceneCoords( + point(event.clientX, event.clientY), + this.state, + ); + const element = this.getElementAtPosition(position, { preferSelected: true, includeLockedElements: true, }); @@ -9773,7 +9699,7 @@ class App extends React.Component { const selectedElements = this.scene.getSelectedElements(this.state); const isHittingCommonBoundBox = this.isHittingCommonBoundingBoxOfSelectedElements( - { x, y }, + position, selectedElements, ); @@ -9827,12 +9753,16 @@ class App extends React.Component { dragNewElement({ newElement: selectionElement, elementType: this.state.activeTool.type, - originX: pointerDownState.origin.x, - originY: pointerDownState.origin.y, - x: pointerCoords.x, - y: pointerCoords.y, - width: distance(pointerDownState.origin.x, pointerCoords.x), - height: distance(pointerDownState.origin.y, pointerCoords.y), + originX: pointerDownState.origin[0], + originY: pointerDownState.origin[1], + x: pointerCoords[0], + y: pointerCoords[1], + width: rangeExtent( + rangeInclusive(pointerDownState.origin[0], pointerCoords[0]), + ), + height: rangeExtent( + rangeInclusive(pointerDownState.origin[1], pointerCoords[1]), + ), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldResizeFromCenter: shouldResizeFromCenter(event), zoom: this.state.zoom.value, @@ -9847,8 +9777,8 @@ class App extends React.Component { } let [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoords[0], + pointerCoords[1], event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -9893,8 +9823,12 @@ class App extends React.Component { originY: pointerDownState.originInGrid.y, x: gridX, y: gridY, - width: distance(pointerDownState.originInGrid.x, gridX), - height: distance(pointerDownState.originInGrid.y, gridY), + width: rangeExtent( + rangeInclusive(pointerDownState.originInGrid.x, gridX), + ), + height: rangeExtent( + rangeInclusive(pointerDownState.originInGrid.y, gridY), + ), shouldMaintainAspectRatio: isImageElement(newElement) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event), @@ -9956,8 +9890,8 @@ class App extends React.Component { }); const pointerCoords = pointerDownState.lastCoords; let [resizeX, resizeY] = getGridPoint( - pointerCoords.x - pointerDownState.resize.offset.x, - pointerCoords.y - pointerDownState.resize.offset.y, + pointerCoords[0] - pointerDownState.resize.offset.x, + pointerCoords[1] - pointerDownState.resize.offset.y, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -9987,8 +9921,8 @@ class App extends React.Component { // during dragging if (!this.state.selectedElementsAreBeingDragged) { const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoords[0], + pointerCoords[1], event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -10195,8 +10129,8 @@ class App extends React.Component { this.translateCanvas((state) => ({ ...getStateForZoom( { - viewportX: this.lastViewportPosition.x, - viewportY: this.lastViewportPosition.y, + viewportX: this.lastViewportPosition[0], + viewportY: this.lastViewportPosition[1], nextZoom: getNormalizedZoom(newZoom), }, state, @@ -10224,36 +10158,37 @@ class App extends React.Component { ); private getTextWysiwygSnappedToCenterPosition( - x: number, - y: number, + position: GlobalPoint, appState: AppState, container?: ExcalidrawTextContainer | null, ) { if (container) { - let elementCenterX = container.x + container.width / 2; - let elementCenterY = container.y + container.height / 2; - - const elementCenter = getContainerCenter( + let elementCenter = point( + container.x + container.width / 2, + container.y + container.height / 2, + ); + const containerCenter = getContainerCenter( container, appState, this.scene.getNonDeletedElementsMap(), ); - if (elementCenter) { - elementCenterX = elementCenter.x; - elementCenterY = elementCenter.y; + if (containerCenter) { + elementCenter = containerCenter; } - const distanceToCenter = Math.hypot( - x - elementCenterX, - y - elementCenterY, - ); + const distanceToCenter = pointDistance(position, elementCenter); const isSnappedToCenter = distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD; if (isSnappedToCenter) { - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( - { sceneX: elementCenterX, sceneY: elementCenterY }, + const [viewportX, viewportY] = sceneCoordsToViewportCoords( + elementCenter, appState, ); - return { viewportX, viewportY, elementCenterX, elementCenterY }; + return { + viewportX, + viewportY, + elementCenterX: elementCenter[0], + elementCenterY: elementCenter[1], + }; } } } @@ -10262,8 +10197,8 @@ class App extends React.Component { if (!x || !y) { return; } - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - { clientX: x, clientY: y }, + const [sceneX, sceneY] = viewportCoordsToSceneCoords( + point(x, y), this.state, ); diff --git a/packages/excalidraw/components/EyeDropper.tsx b/packages/excalidraw/components/EyeDropper.tsx index f076976627..300d8a077e 100644 --- a/packages/excalidraw/components/EyeDropper.tsx +++ b/packages/excalidraw/components/EyeDropper.tsx @@ -164,8 +164,8 @@ export const EyeDropper: React.FC<{ // init color preview else it would show only after the first mouse move mouseMoveListener({ - clientX: stableProps.app.lastViewportPosition.x, - clientY: stableProps.app.lastViewportPosition.y, + clientX: stableProps.app.lastViewportPosition[0], + clientY: stableProps.app.lastViewportPosition[1], altKey: false, }); diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index bf20ea0588..93fbedd393 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -15,6 +15,7 @@ import type { } from "../../element/types"; import { isRenderThrottlingEnabled } from "../../reactUtils"; import { renderInteractiveScene } from "../../renderer/interactiveScene"; +import { point } from "../../../math"; type InteractiveCanvasProps = { containerRef: React.RefObject; @@ -103,10 +104,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { remotePointerViewportCoords.set( socketId, sceneCoordsToViewportCoords( - { - sceneX: user.pointer.x, - sceneY: user.pointer.y, - }, + point(user.pointer.x, user.pointer.y), props.appState, ), ); diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx index cb11c46cac..4e8703116e 100644 --- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx +++ b/packages/excalidraw/components/hyperlink/Hyperlink.tsx @@ -36,7 +36,8 @@ 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 type { ViewportPoint } from "../../../math"; +import { point } from "../../../math"; const CONTAINER_WIDTH = 320; const SPACE_BOTTOM = 85; @@ -324,8 +325,8 @@ const getCoordsForPopover = ( elementsMap: ElementsMap, ) => { const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( - { sceneX: x1 + element.width / 2, sceneY: y1 }, + const [viewportX, viewportY] = sceneCoordsToViewportCoords( + point(x1 + element.width / 2, y1), appState, ); const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2; @@ -387,15 +388,15 @@ const renderTooltip = ( ); const linkViewportCoords = sceneCoordsToViewportCoords( - { sceneX: linkX, sceneY: linkY }, + point(linkX, linkY), appState, ); updateTooltipPosition( tooltipDiv, { - left: linkViewportCoords.x, - top: linkViewportCoords.y, + left: linkViewportCoords[0], + top: linkViewportCoords[1], width: linkWidth, height: linkHeight, }, @@ -419,25 +420,22 @@ const shouldHideLinkPopup = ( element: NonDeletedExcalidrawElement, elementsMap: ElementsMap, appState: AppState, - [clientX, clientY]: GlobalPoint, + viewportCoords: ViewportPoint, ): Boolean => { - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - { clientX, clientY }, - appState, - ); + const sceneCoords = viewportCoordsToSceneCoords(viewportCoords, appState); const threshold = 15 / appState.zoom.value; // hitbox to prevent hiding when hovered in element bounding box - if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) { + if (hitElementBoundingBox(sceneCoords, element, elementsMap)) { return false; } const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap); // hit box to prevent hiding when hovered in the vertical area between element and popover if ( - sceneX >= x1 && - sceneX <= x2 && - sceneY >= y1 - SPACE_BOTTOM && - sceneY <= y1 + sceneCoords[0] >= x1 && + sceneCoords[0] <= x2 && + sceneCoords[1] >= y1 - SPACE_BOTTOM && + sceneCoords[1] <= y1 ) { return false; } @@ -449,10 +447,12 @@ const shouldHideLinkPopup = ( ); if ( - clientX >= popoverX - threshold && - clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold && - clientY >= popoverY - threshold && - clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT + viewportCoords[0] >= popoverX - threshold && + viewportCoords[0] <= + popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold && + viewportCoords[1] >= popoverY - threshold && + viewportCoords[1] <= + popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT ) { return false; } diff --git a/packages/excalidraw/components/hyperlink/helpers.ts b/packages/excalidraw/components/hyperlink/helpers.ts index 3f451a2305..efa5862f0c 100644 --- a/packages/excalidraw/components/hyperlink/helpers.ts +++ b/packages/excalidraw/components/hyperlink/helpers.ts @@ -81,7 +81,7 @@ export const isPointHittingLink = ( if ( !isMobile && appState.viewModeEnabled && - hitElementBoundingBox(x, y, element, elementsMap) + hitElementBoundingBox(point(x, y), element, elementsMap) ) { return true; } diff --git a/packages/excalidraw/element/ElementCanvasButtons.tsx b/packages/excalidraw/element/ElementCanvasButtons.tsx index 1bcad97b4e..22b2d87b9d 100644 --- a/packages/excalidraw/element/ElementCanvasButtons.tsx +++ b/packages/excalidraw/element/ElementCanvasButtons.tsx @@ -5,6 +5,7 @@ import { getElementAbsoluteCoords } from "."; import { useExcalidrawAppState } from "../components/App"; import "./ElementCanvasButtons.scss"; +import { point } from "../../math"; const CONTAINER_PADDING = 5; @@ -14,8 +15,8 @@ const getContainerCoords = ( elementsMap: ElementsMap, ) => { const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( - { sceneX: x1 + element.width, sceneY: y1 }, + const [viewportX, viewportY] = sceneCoordsToViewportCoords( + point(x1 + element.width, y1), appState, ); const x = viewportX - appState.offsetLeft + 10; diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 62e66f645b..3e273b91dd 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -49,7 +49,7 @@ import type { ElementUpdate } from "./mutateElement"; import { mutateElement } from "./mutateElement"; import type Scene from "../scene/Scene"; import { LinearElementEditor } from "./linearElementEditor"; -import { arrayToMap, tupleToCoors } from "../utils"; +import { arrayToMap } from "../utils"; import { KEYS } from "../keys"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes"; @@ -389,7 +389,7 @@ export const getSuggestedBindingsForArrows = ( export const maybeBindLinearElement = ( linearElement: NonDeleted, appState: AppState, - pointerCoords: { x: number; y: number }, + pointerCoords: GlobalPoint, elementsMap: NonDeletedSceneElementsMap, elements: readonly NonDeletedExcalidrawElement[], ): void => { @@ -508,10 +508,7 @@ const unbindLinearElement = ( }; export const getHoveredElementForBinding = ( - pointerCoords: { - x: number; - y: number; - }, + pointer: GlobalPoint, elements: readonly NonDeletedExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap, fullShape?: boolean, @@ -522,7 +519,7 @@ export const getHoveredElementForBinding = ( isBindableElement(element, false) && bindingBorderTest( element, - pointerCoords, + pointer, elementsMap, // disable fullshape snapping for frame elements so we // can bind to frame children @@ -1177,14 +1174,12 @@ const getLinearElementEdgeCoors = ( linearElement: NonDeleted, startOrEnd: "start" | "end", elementsMap: NonDeletedSceneElementsMap, -): { x: number; y: number } => { +): GlobalPoint => { const index = startOrEnd === "start" ? 0 : -1; - return tupleToCoors( - LinearElementEditor.getPointAtIndexGlobalCoordinates( - linearElement, - index, - elementsMap, - ), + return LinearElementEditor.getPointAtIndexGlobalCoordinates( + linearElement, + index, + elementsMap, ); }; @@ -1330,7 +1325,7 @@ const newBoundElements = ( export const bindingBorderTest = ( element: NonDeleted, - { x, y }: { x: number; y: number }, + [x, y]: GlobalPoint, elementsMap: NonDeletedSceneElementsMap, fullShape?: boolean, ): boolean => { diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 7eafa7dfa0..e7c7f334bd 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -42,35 +42,33 @@ export const shouldTestInside = (element: ExcalidrawElement) => { }; export type HitTestArgs = { - x: number; - y: number; + sceneCoords: Point; element: ExcalidrawElement; shape: GeometricShape; threshold?: number; frameNameBound?: FrameNameBounds | null; }; -export const hitElementItself = ({ - x, - y, +export const hitElementItself = ({ + sceneCoords, element, shape, threshold = 10, frameNameBound = null, -}: HitTestArgs) => { +}: HitTestArgs) => { 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(sceneCoords, shape) || + isPointOnShape(sceneCoords, shape, threshold) + : isPointOnShape(sceneCoords, shape, threshold); // hit test against a frame's name if (!hit && frameNameBound) { - hit = isPointInShape(point(x, y), { + hit = isPointInShape(sceneCoords, { type: "polygon", data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) - .data as Polygon, + .data as Polygon, }); } @@ -78,8 +76,7 @@ export const hitElementItself = ({ }; export const hitElementBoundingBox = ( - x: number, - y: number, + scenePointer: GlobalPoint, element: ExcalidrawElement, elementsMap: ElementsMap, tolerance = 0, @@ -89,31 +86,27 @@ export const hitElementBoundingBox = ( y1 -= tolerance; x2 += tolerance; y2 += tolerance; - return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2)); + return isPointWithinBounds(point(x1, y1), scenePointer, point(x2, y2)); }; -export const hitElementBoundingBoxOnly = < - Point extends GlobalPoint | LocalPoint, ->( - hitArgs: HitTestArgs, +export const hitElementBoundingBoxOnly = ( + hitArgs: HitTestArgs, elementsMap: ElementsMap, ) => { return ( !hitElementItself(hitArgs) && // bound text is considered part of the element (even if it's outside the bounding box) !hitElementBoundText( - hitArgs.x, - hitArgs.y, + hitArgs.sceneCoords, getBoundTextShape(hitArgs.element, elementsMap), ) && - hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap) + hitElementBoundingBox(hitArgs.sceneCoords, hitArgs.element, elementsMap) ); }; -export const hitElementBoundText = ( - x: number, - y: number, - textShape: GeometricShape | null, +export const hitElementBoundText = ( + scenePointer: GlobalPoint, + textShape: GeometricShape | null, ): boolean => { - return !!textShape && isPointInShape(point(x, y), textShape); + return !!textShape && isPointInShape(scenePointer, textShape); }; diff --git a/packages/excalidraw/element/heading.ts b/packages/excalidraw/element/heading.ts index b22316c6a2..0931712a87 100644 --- a/packages/excalidraw/element/heading.ts +++ b/packages/excalidraw/element/heading.ts @@ -4,6 +4,7 @@ import type { Triangle, Vector, Radians, + ViewportPoint, } from "../../math"; import { point, @@ -21,7 +22,9 @@ export const HEADING_LEFT = [-1, 0] as Heading; export const HEADING_UP = [0, -1] as Heading; export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1]; -export const headingForDiamond = ( +export const headingForDiamond = < + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>( a: Point, b: Point, ) => { diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index e11c0b158c..357bd76206 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -20,7 +20,6 @@ import { } from "./bounds"; import type { AppState, - PointerCoords, InteractiveCanvasAppState, AppClassProperties, NullableGridSize, @@ -32,7 +31,7 @@ import { getHoveredElementForBinding, isBindingEnabled, } from "./binding"; -import { invariant, toBrandedType, tupleToCoors } from "../utils"; +import { invariant, toBrandedType } from "../utils"; import { isBindingElement, isElbowArrow, @@ -56,6 +55,8 @@ import { type GlobalPoint, type LocalPoint, pointDistance, + pointSubtract, + pointFromPair, } from "../../math"; import { getBezierCurveLength, @@ -83,7 +84,7 @@ export class LinearElementEditor { /** index */ lastClickedPoint: number; lastClickedIsEndPoint: boolean; - origin: Readonly<{ x: number; y: number }> | null; + origin: GlobalPoint | null; segmentMidpoint: { value: GlobalPoint | null; index: number | null; @@ -94,7 +95,7 @@ export class LinearElementEditor { /** whether you're dragging a point */ public readonly isDragging: boolean; public readonly lastUncommittedPoint: LocalPoint | null; - public readonly pointerOffset: Readonly<{ x: number; y: number }>; + public readonly pointerOffset: GlobalPoint; public readonly startBindingElement: | ExcalidrawBindableElement | null @@ -115,7 +116,7 @@ export class LinearElementEditor { this.selectedPointsIndices = null; this.lastUncommittedPoint = null; this.isDragging = false; - this.pointerOffset = { x: 0, y: 0 }; + this.pointerOffset = point(0, 0); this.startBindingElement = "keep"; this.endBindingElement = "keep"; this.pointerDownState = { @@ -219,11 +220,10 @@ export class LinearElementEditor { static handlePointDragging( event: PointerEvent, app: AppClassProperties, - scenePointerX: number, - scenePointerY: number, + scenePointer: GlobalPoint, maybeSuggestBinding: ( element: NonDeleted, - pointSceneCoords: { x: number; y: number }[], + pointSceneCoords: GlobalPoint[], ) => void, linearElementEditor: LinearElementEditor, scene: Scene, @@ -287,7 +287,7 @@ export class LinearElementEditor { element, elementsMap, referencePoint, - point(scenePointerX, scenePointerY), + scenePointer, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -309,8 +309,7 @@ export class LinearElementEditor { const newDraggingPointPosition = LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, + pointSubtract(scenePointer, linearElementEditor.pointerOffset), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -325,8 +324,10 @@ export class LinearElementEditor { ? LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, + pointSubtract( + scenePointer, + linearElementEditor.pointerOffset, + ), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ) : point( @@ -350,17 +351,15 @@ export class LinearElementEditor { // suggest bindings for first and last point if selected if (isBindingElement(element, false)) { - const coords: { x: number; y: number }[] = []; + const coords: GlobalPoint[] = []; const firstSelectedIndex = selectedPointsIndices[0]; if (firstSelectedIndex === 0) { coords.push( - tupleToCoors( - LinearElementEditor.getPointGlobalCoordinates( - element, - element.points[0], - elementsMap, - ), + LinearElementEditor.getPointGlobalCoordinates( + element, + element.points[0], + elementsMap, ), ); } @@ -369,12 +368,10 @@ export class LinearElementEditor { selectedPointsIndices[selectedPointsIndices.length - 1]; if (lastSelectedIndex === element.points.length - 1) { coords.push( - tupleToCoors( - LinearElementEditor.getPointGlobalCoordinates( - element, - element.points[lastSelectedIndex], - elementsMap, - ), + LinearElementEditor.getPointGlobalCoordinates( + element, + element.points[lastSelectedIndex], + elementsMap, ), ); } @@ -439,12 +436,10 @@ export class LinearElementEditor { const bindingElement = isBindingEnabled(appState) ? getHoveredElementForBinding( - tupleToCoors( - LinearElementEditor.getPointAtIndexGlobalCoordinates( - element, - selectedPoint!, - elementsMap, - ), + LinearElementEditor.getPointAtIndexGlobalCoordinates( + element, + selectedPoint!, + elementsMap, ), elements, elementsMap, @@ -481,7 +476,7 @@ export class LinearElementEditor { ? [pointerDownState.lastClickedPoint] : selectedPointsIndices, isDragging: false, - pointerOffset: { x: 0, y: 0 }, + pointerOffset: point(0, 0), }; } @@ -556,7 +551,7 @@ export class LinearElementEditor { static getSegmentMidpointHitCoords = ( linearElementEditor: LinearElementEditor, - scenePointer: { x: number; y: number }, + scenePointer: GlobalPoint, appState: AppState, elementsMap: ElementsMap, ): GlobalPoint | null => { @@ -569,8 +564,7 @@ export class LinearElementEditor { element, elementsMap, appState.zoom, - scenePointer.x, - scenePointer.y, + scenePointer, ); if (clickedPointIndex >= 0) { return null; @@ -594,7 +588,7 @@ export class LinearElementEditor { existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[1], ), - point(scenePointer.x, scenePointer.y), + scenePointer, ); if (distance <= threshold) { return existingSegmentMidpointHitCoords; @@ -607,7 +601,7 @@ export class LinearElementEditor { if (midPoints[index] !== null) { const distance = pointDistance( point(midPoints[index]![0], midPoints[index]![1]), - point(scenePointer.x, scenePointer.y), + scenePointer, ); if (distance <= threshold) { return midPoints[index]; @@ -705,7 +699,7 @@ export class LinearElementEditor { event: React.PointerEvent, app: AppClassProperties, store: Store, - scenePointer: { x: number; y: number }, + scenePointer: GlobalPoint, linearElementEditor: LinearElementEditor, scene: Scene, ): { @@ -759,8 +753,7 @@ export class LinearElementEditor { LinearElementEditor.createPointAt( element, elementsMap, - scenePointer.x, - scenePointer.y, + scenePointer, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ), ], @@ -774,7 +767,7 @@ export class LinearElementEditor { prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, lastClickedPoint: -1, lastClickedIsEndPoint: false, - origin: { x: scenePointer.x, y: scenePointer.y }, + origin: scenePointer, segmentMidpoint: { value: segmentMidpoint, index: segmentMidpointIndex, @@ -798,8 +791,7 @@ export class LinearElementEditor { element, elementsMap, appState.zoom, - scenePointer.x, - scenePointer.y, + scenePointer, ); // if we clicked on a point, set the element as hitElement otherwise // it would get deselected if the point is outside the hitbox area @@ -828,7 +820,7 @@ export class LinearElementEditor { const cy = (y1 + y2) / 2; const targetPoint = clickedPointIndex > -1 && - pointRotateRads( + pointRotateRads( point( element.x + element.points[clickedPointIndex][0], element.y + element.points[clickedPointIndex][1], @@ -853,7 +845,7 @@ export class LinearElementEditor { prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, lastClickedPoint: clickedPointIndex, lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1, - origin: { x: scenePointer.x, y: scenePointer.y }, + origin: scenePointer, segmentMidpoint: { value: segmentMidpoint, index: segmentMidpointIndex, @@ -862,11 +854,8 @@ export class LinearElementEditor { }, selectedPointsIndices: nextSelectedPointsIndices, pointerOffset: targetPoint - ? { - x: scenePointer.x - targetPoint[0], - y: scenePointer.y - targetPoint[1], - } - : { x: 0, y: 0 }, + ? pointSubtract(scenePointer, targetPoint) + : point(0, 0), }; return ret; @@ -887,8 +876,7 @@ export class LinearElementEditor { static handlePointerMove( event: React.PointerEvent, - scenePointerX: number, - scenePointerY: number, + scenePointer: GlobalPoint, app: AppClassProperties, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, ): LinearElementEditor | null { @@ -928,7 +916,7 @@ export class LinearElementEditor { element, elementsMap, lastCommittedPoint, - point(scenePointerX, scenePointerY), + scenePointer, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -940,8 +928,10 @@ export class LinearElementEditor { newPoint = LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - appState.editingLinearElement.pointerOffset.x, - scenePointerY - appState.editingLinearElement.pointerOffset.y, + pointSubtract( + scenePointer, + appState.editingLinearElement.pointerOffset, + ), event[KEYS.CTRL_OR_CMD] || isElbowArrow(element) ? null : app.getEffectiveGridSize(), @@ -1057,8 +1047,7 @@ export class LinearElementEditor { element: NonDeleted, elementsMap: ElementsMap, zoom: AppState["zoom"], - x: number, - y: number, + p: GlobalPoint, ) { const pointHandles = LinearElementEditor.getPointsGlobalCoordinates( element, @@ -1069,9 +1058,9 @@ export class LinearElementEditor { // points on the left, thus should take precedence when clicking, if they // overlap while (--idx > -1) { - const p = pointHandles[idx]; + const handles = pointHandles[idx]; if ( - pointDistance(point(x, y), point(p[0], p[1])) * zoom.value < + pointDistance(p, pointFromPair(handles)) * zoom.value < // +1px to account for outline stroke LinearElementEditor.POINT_HANDLE_SIZE + 1 ) { @@ -1084,11 +1073,14 @@ export class LinearElementEditor { static createPointAt( element: NonDeleted, elementsMap: ElementsMap, - scenePointerX: number, - scenePointerY: number, + scenePointer: GlobalPoint, gridSize: NullableGridSize, ): LocalPoint { - const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); + const pointerOnGrid = getGridPoint( + scenePointer[0], + scenePointer[1], + gridSize, + ); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; @@ -1337,7 +1329,7 @@ export class LinearElementEditor { static shouldAddMidpoint( linearElementEditor: LinearElementEditor, - pointerCoords: PointerCoords, + pointerCoords: GlobalPoint, appState: AppState, elementsMap: ElementsMap, ) { @@ -1367,10 +1359,7 @@ export class LinearElementEditor { } const origin = linearElementEditor.pointerDownState.origin!; - const dist = pointDistance( - point(origin.x, origin.y), - point(pointerCoords.x, pointerCoords.y), - ); + const dist = pointDistance(origin, pointerCoords); if ( !appState.editingLinearElement && dist < DRAGGING_THRESHOLD / appState.zoom.value @@ -1382,7 +1371,7 @@ export class LinearElementEditor { static addMidpoint( linearElementEditor: LinearElementEditor, - pointerCoords: PointerCoords, + pointerCoords: GlobalPoint, app: AppClassProperties, snapToGrid: boolean, elementsMap: ElementsMap, @@ -1406,8 +1395,7 @@ export class LinearElementEditor { const midpoint = LinearElementEditor.createPointAt( element, elementsMap, - pointerCoords.x, - pointerCoords.y, + pointerCoords, snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null, ); const points = [ diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 0a01459e69..4b5a481a53 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -1089,8 +1089,7 @@ export const getResizeOffsetXY = ( transformHandleType: MaybeTransformHandleType, selectedElements: NonDeletedExcalidrawElement[], elementsMap: ElementsMap, - x: number, - y: number, + [x, y]: GlobalPoint, ): [number, number] => { const [x1, y1, x2, y2] = selectedElements.length === 1 diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts index c363f61806..55e5cca87e 100644 --- a/packages/excalidraw/element/resizeTest.ts +++ b/packages/excalidraw/element/resizeTest.ts @@ -92,7 +92,7 @@ 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(x1 - SPACING, y1 - SPACING), point(x2 + SPACING, y2 + SPACING), point(cx, cy), element.angle, @@ -101,7 +101,11 @@ export const resizeTest = ( 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( + point(x, y), + side as LineSegment, + SPACING, + ) ) { return dir as TransformHandleType; } @@ -115,8 +119,7 @@ export const resizeTest = ( export const getElementWithTransformHandleType = ( elements: readonly NonDeletedExcalidrawElement[], appState: AppState, - scenePointerX: number, - scenePointerY: number, + scenePointer: GlobalPoint, zoom: Zoom, pointerType: PointerType, elementsMap: ElementsMap, @@ -130,8 +133,8 @@ export const getElementWithTransformHandleType = ( element, elementsMap, appState, - scenePointerX, - scenePointerY, + scenePointer[0], + scenePointer[1], zoom, pointerType, device, @@ -140,12 +143,9 @@ export const getElementWithTransformHandleType = ( }, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null); }; -export const getTransformHandleTypeFromCoords = < - Point extends GlobalPoint | LocalPoint, ->( +export const getTransformHandleTypeFromCoords = ( [x1, y1, x2, y2]: Bounds, - scenePointerX: number, - scenePointerY: number, + scenePointer: GlobalPoint, zoom: Zoom, pointerType: PointerType, device: Device, @@ -163,7 +163,7 @@ export const getTransformHandleTypeFromCoords = < transformHandles[key as Exclude]!; return ( transformHandle && - isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY) + isInsideTransformHandle(transformHandle, scenePointer[0], scenePointer[1]) ); }); @@ -178,7 +178,7 @@ export const getTransformHandleTypeFromCoords = < const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const sides = getSelectionBorders( - point(x1 - SPACING, y1 - SPACING), + point(x1 - SPACING, y1 - SPACING), point(x2 + SPACING, y2 + SPACING), point(cx, cy), 0 as Radians, @@ -188,8 +188,8 @@ export const getTransformHandleTypeFromCoords = < // test to see if x, y are on the line segment if ( pointOnLineSegment( - point(scenePointerX, scenePointerY), - side as LineSegment, + scenePointer, + side as LineSegment, SPACING, ) ) { diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index 895340c91a..7efedd78b9 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -14,7 +14,7 @@ import { import BinaryHeap from "../binaryheap"; import { getSizeFromPoints } from "../points"; import { aabbForElement, pointInsideBounds } from "../shapes"; -import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils"; +import { isAnyTrue, toBrandedType } from "../utils"; import { bindPointToSnapToElementOutline, distanceToBindableElement, @@ -1081,13 +1081,13 @@ const getHoveredElements = ( const elements = Array.from(elementsMap.values()); return [ getHoveredElementForBinding( - tupleToCoors(origStartGlobalPoint), + origStartGlobalPoint, elements, nonDeletedSceneElementsMap, true, ), getHoveredElementForBinding( - tupleToCoors(origEndGlobalPoint), + origEndGlobalPoint, elements, nonDeletedSceneElementsMap, true, diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts index f633789a9d..7a1deb462d 100644 --- a/packages/excalidraw/element/sizeHelpers.ts +++ b/packages/excalidraw/element/sizeHelpers.ts @@ -5,6 +5,7 @@ import { SHIFT_LOCKING_ANGLE } from "../constants"; import type { AppState, Offsets, Zoom } from "../types"; import { getCommonBounds, getElementBounds } from "./bounds"; import { viewportCoordsToSceneCoords } from "../utils"; +import { point } from "../../math"; // TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted // - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize' @@ -33,25 +34,22 @@ export const isElementInViewport = ( ) => { const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates const topLeftSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft, - clientY: viewTransformations.offsetTop, - }, + point(viewTransformations.offsetLeft, viewTransformations.offsetTop), viewTransformations, ); const bottomRightSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + width, - clientY: viewTransformations.offsetTop + height, - }, + point( + viewTransformations.offsetLeft + width, + viewTransformations.offsetTop + height, + ), viewTransformations, ); return ( - topLeftSceneCoords.x <= x2 && - topLeftSceneCoords.y <= y2 && - bottomRightSceneCoords.x >= x1 && - bottomRightSceneCoords.y >= y1 + topLeftSceneCoords[0] <= x2 && + topLeftSceneCoords[1] <= y2 && + bottomRightSceneCoords[0] >= x1 && + bottomRightSceneCoords[1] >= y1 ); }; @@ -71,25 +69,25 @@ export const isElementCompletelyInViewport = ( ) => { const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates const topLeftSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + (padding?.left || 0), - clientY: viewTransformations.offsetTop + (padding?.top || 0), - }, + point( + viewTransformations.offsetLeft + (padding?.left || 0), + viewTransformations.offsetTop + (padding?.top || 0), + ), viewTransformations, ); const bottomRightSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + width - (padding?.right || 0), - clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0), - }, + point( + viewTransformations.offsetLeft + width - (padding?.right || 0), + viewTransformations.offsetTop + height - (padding?.bottom || 0), + ), viewTransformations, ); return ( - x1 >= topLeftSceneCoords.x && - y1 >= topLeftSceneCoords.y && - x2 <= bottomRightSceneCoords.x && - y2 <= bottomRightSceneCoords.y + x1 >= topLeftSceneCoords[0] && + y1 >= topLeftSceneCoords[1] && + x2 <= bottomRightSceneCoords[0] && + y2 <= bottomRightSceneCoords[1] ); }; diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index 9abebc3563..8a1bb21362 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -29,6 +29,8 @@ import { updateOriginalContainerCache, } from "./containerCache"; import type { ExtractSetType } from "../utility-types"; +import type { GlobalPoint } from "../../math"; +import { point } from "../../math"; export const normalizeText = (text: string) => { return ( @@ -674,12 +676,12 @@ export const getContainerCenter = ( container: ExcalidrawElement, appState: AppState, elementsMap: ElementsMap, -) => { +): GlobalPoint => { if (!isArrowElement(container)) { - return { - x: container.x + container.width / 2, - y: container.y + container.height / 2, - }; + return point( + container.x + container.width / 2, + container.y + container.height / 2, + ); } const points = LinearElementEditor.getPointsGlobalCoordinates( container, @@ -692,7 +694,7 @@ export const getContainerCenter = ( container.points[index], elementsMap, ); - return { x: midPoint[0], y: midPoint[1] }; + return point(midPoint[0], midPoint[1]); } const index = container.points.length / 2 - 1; let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints( @@ -709,7 +711,7 @@ export const getContainerCenter = ( elementsMap, ); } - return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; + return point(midSegmentMidpoint[0], midSegmentMidpoint[1]); }; export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index fb9a45820f..a9736a96ac 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -29,6 +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 type { GlobalPoint } from "../math"; import { isPointWithinBounds, point } from "../math"; // --------------------------- Frame State ------------------------------------ @@ -149,20 +150,13 @@ export const elementOverlapsWithFrame = ( }; export const isCursorInFrame = ( - cursorCoords: { - x: number; - y: number; - }, + cursorCoords: GlobalPoint, frame: NonDeleted, elementsMap: ElementsMap, ) => { const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); - return isPointWithinBounds( - point(fx1, fy1), - point(cursorCoords.x, cursorCoords.y), - point(fx2, fy2), - ); + return isPointWithinBounds(point(fx1, fy1), cursorCoords, point(fx2, fy2)); }; export const groupsAreAtLeastIntersectingTheFrame = ( diff --git a/packages/excalidraw/gesture.ts b/packages/excalidraw/gesture.ts deleted file mode 100644 index 8ffa6d2bbc..0000000000 --- a/packages/excalidraw/gesture.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { PointerCoords } from "./types"; - -export const getCenter = (pointers: Map) => { - const allCoords = Array.from(pointers.values()); - return { - x: sum(allCoords, (coords) => coords.x) / allCoords.length, - y: sum(allCoords, (coords) => coords.y) / allCoords.length, - }; -}; - -export const getDistance = ([a, b]: readonly PointerCoords[]) => - Math.hypot(a.x - b.x, a.y - b.y); - -const sum = (array: readonly T[], mapper: (item: T) => number): number => - array.reduce((acc, item) => acc + mapper(item), 0); diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 9995c748ac..031a790c80 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -26,7 +26,7 @@ import type { RenderableElementsMap, InteractiveCanvasRenderConfig, } from "../scene/types"; -import { distance, getFontString, isRTL } from "../utils"; +import { getFontString, isRTL } from "../utils"; import rough from "roughjs/bin/rough"; import type { AppState, @@ -59,7 +59,7 @@ import { LinearElementEditor } from "../element/linearElementEditor"; import { getContainingFrame } from "../frame"; import { ShapeCache } from "../scene/ShapeCache"; import { getVerticalOffset } from "../fonts"; -import { isRightAngleRads } from "../../math"; +import { isRightAngleRads, rangeExtent, rangeInclusive } from "../../math"; import { getCornerRadius } from "../shapes"; // using a stronger invert (100% vs our regular 93%) and saturate @@ -163,11 +163,11 @@ const cappedElementCanvasSize = ( const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const elementWidth = isLinearElement(element) || isFreeDrawElement(element) - ? distance(x1, x2) + ? rangeExtent(rangeInclusive(x1, x2)) : element.width; const elementHeight = isLinearElement(element) || isFreeDrawElement(element) - ? distance(y1, y2) + ? rangeExtent(rangeInclusive(y1, y2)) : element.height; let width = elementWidth * window.devicePixelRatio + padding * 2; @@ -226,12 +226,16 @@ const generateElementCanvas = ( canvasOffsetX = element.x > x1 - ? distance(element.x, x1) * window.devicePixelRatio * scale + ? rangeExtent(rangeInclusive(element.x, x1)) * + window.devicePixelRatio * + scale : 0; canvasOffsetY = element.y > y1 - ? distance(element.y, y1) * window.devicePixelRatio * scale + ? rangeExtent(rangeInclusive(element.y, y1)) * + window.devicePixelRatio * + scale : 0; context.translate(canvasOffsetX, canvasOffsetY); @@ -263,7 +267,10 @@ const generateElementCanvas = ( const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); // Take max dimensions of arrow canvas so that when canvas is rotated // the arrow doesn't get clipped - const maxDim = Math.max(distance(x1, x2), distance(y1, y2)); + const maxDim = Math.max( + rangeExtent(rangeInclusive(x1, x2)), + rangeExtent(rangeInclusive(y1, y2)), + ); boundTextCanvas.width = maxDim * window.devicePixelRatio * scale + padding * scale * 10; boundTextCanvas.height = @@ -813,7 +820,10 @@ export const renderElement = ( // Take max dimensions of arrow canvas so that when canvas is rotated // the arrow doesn't get clipped - const maxDim = Math.max(distance(x1, x2), distance(y1, y2)); + const maxDim = Math.max( + rangeExtent(rangeInclusive(x1, x2)), + rangeExtent(rangeInclusive(y1, y2)), + ); const padding = getCanvasPadding(element); tempCanvas.width = maxDim * appState.exportScale + padding * 10 * appState.exportScale; diff --git a/packages/excalidraw/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts index 33b57ce686..fc6b8bb80d 100644 --- a/packages/excalidraw/renderer/renderSnaps.ts +++ b/packages/excalidraw/renderer/renderSnaps.ts @@ -1,3 +1,4 @@ +import type { ViewportPoint } from "../../math"; import { point, type GlobalPoint, type LocalPoint } from "../../math"; import { THEME } from "../constants"; import type { PointSnapLine, PointerSnapLine } from "../snapping"; @@ -107,7 +108,7 @@ const drawCross = ( context.restore(); }; -const drawLine = ( +const drawLine = ( from: Point, to: Point, context: CanvasRenderingContext2D, @@ -118,7 +119,7 @@ const drawLine = ( context.stroke(); }; -const drawGapLine = ( +const drawGapLine = ( from: Point, to: Point, direction: "horizontal" | "vertical", diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 6d1b963fcc..72bef0d9b8 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -9,7 +9,7 @@ import type { import type { Bounds } from "../element/bounds"; import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds"; import { renderSceneToSvg } from "../renderer/staticSvgScene"; -import { arrayToMap, distance, getFontString, toBrandedType } from "../utils"; +import { arrayToMap, getFontString, toBrandedType } from "../utils"; import type { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, @@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex"; import { renderStaticScene } from "../renderer/staticScene"; import { Fonts } from "../fonts"; import type { Font } from "../fonts/ExcalidrawFont"; +import { rangeExtent, rangeInclusive } from "../../math"; const SVG_EXPORT_TAG = ``; @@ -427,8 +428,8 @@ const getCanvasSize = ( exportPadding: number, ): Bounds => { const [minX, minY, maxX, maxY] = getCommonBounds(elements); - const width = distance(minX, maxX) + exportPadding * 2; - const height = distance(minY, maxY) + exportPadding * 2; + const width = rangeExtent(rangeInclusive(minX, maxX)) + exportPadding * 2; + const height = rangeExtent(rangeInclusive(minY, maxY)) + exportPadding * 2; return [minX, minY, width, height]; }; diff --git a/packages/excalidraw/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts index 5d059e5b44..9fe7dfda24 100644 --- a/packages/excalidraw/scene/scroll.ts +++ b/packages/excalidraw/scene/scroll.ts @@ -1,4 +1,4 @@ -import type { AppState, Offsets, PointerCoords, Zoom } from "../types"; +import type { AppState, Offsets, Zoom } from "../types"; import type { ExcalidrawElement } from "../element/types"; import { getCommonBounds, @@ -8,17 +8,19 @@ import { import { sceneCoordsToViewportCoords, + tupleToCoors, viewportCoordsToSceneCoords, } from "../utils"; +import { point, type GlobalPoint } from "../../math"; const isOutsideViewPort = (appState: AppState, cords: Array) => { const [x1, y1, x2, y2] = cords; - const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords( - { sceneX: x1, sceneY: y1 }, + const [viewportX1, viewportY1] = sceneCoordsToViewportCoords( + point(x1, y1), appState, ); - const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords( - { sceneX: x2, sceneY: y2 }, + const [viewportX2, viewportY2] = sceneCoordsToViewportCoords( + point(x2, y2), appState, ); return ( @@ -33,20 +35,20 @@ export const centerScrollOn = ({ zoom, offsets, }: { - scenePoint: PointerCoords; + scenePoint: GlobalPoint; viewportDimensions: { height: number; width: number }; zoom: Zoom; offsets?: Offsets; }) => { let scrollX = (viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value - - scenePoint.x; + scenePoint[0]; scrollX += (offsets?.left ?? 0) / 2 / zoom.value; let scrollY = (viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value - - scenePoint.y; + scenePoint[1]; scrollY += (offsets?.top ?? 0) / 2 / zoom.value; @@ -73,9 +75,11 @@ export const calculateScrollCenter = ( if (isOutsideViewPort(appState, [x1, y1, x2, y2])) { [x1, y1, x2, y2] = getClosestElementBounds( elements, - viewportCoordsToSceneCoords( - { clientX: appState.scrollX, clientY: appState.scrollY }, - appState, + tupleToCoors( + viewportCoordsToSceneCoords( + point(appState.scrollX, appState.scrollY), + appState, + ), ), ); } @@ -84,7 +88,7 @@ export const calculateScrollCenter = ( const centerY = (y1 + y2) / 2; return centerScrollOn({ - scenePoint: { x: centerX, y: centerY }, + scenePoint: point(centerX, centerY), viewportDimensions: { width: appState.width, height: appState.height }, zoom: appState.zoom, }); diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 67ab3e3abc..57245b6fe3 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -19,6 +19,7 @@ import type { PendingExcalidrawElements, } from "../types"; import type { MakeBrand } from "../utility-types"; +import type { ViewportPoint } from "../../math"; export type RenderableElementsMap = NonDeletedElementsMap & MakeBrand<"RenderableElementsMap">; @@ -52,7 +53,7 @@ export type InteractiveCanvasRenderConfig = { // collab-related state // --------------------------------------------------------------------------- remoteSelectedElementIds: Map; - remotePointerViewportCoords: Map; + remotePointerViewportCoords: Map; remotePointerUserStates: Map; remotePointerUsernames: Map; remotePointerButton: Map; diff --git a/packages/excalidraw/shapes.tsx b/packages/excalidraw/shapes.tsx index 2c935145c8..eb8403a667 100644 --- a/packages/excalidraw/shapes.tsx +++ b/packages/excalidraw/shapes.tsx @@ -1,3 +1,4 @@ +import type { ViewportPoint } from "../math"; import { isPoint, point, @@ -435,7 +436,9 @@ export const aabbForElement = ( return bounds; }; -export const pointInsideBounds =

( +export const pointInsideBounds = < + P extends GlobalPoint | LocalPoint | ViewportPoint, +>( p: P, bounds: Bounds, ): boolean => diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index c4ebd994e0..24f00b7cb2 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -41,6 +41,7 @@ import type { ContextMenuItems } from "./components/ContextMenu"; import type { SnapLine } from "./snapping"; import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types"; import type { StoreActionType } from "./store"; +import type { GlobalPoint } from "../math"; export type SocketId = string & { _brand: "SocketId" }; @@ -415,14 +416,9 @@ export type Zoom = Readonly<{ value: NormalizedZoomValue; }>; -export type PointerCoords = Readonly<{ - x: number; - y: number; -}>; - export type Gesture = { - pointers: Map; - lastCenter: { x: number; y: number } | null; + pointers: Map; + lastCenter: GlobalPoint | null; initialDistance: number | null; initialScale: number | null; }; @@ -661,17 +657,17 @@ export type AppClassProperties = { excalidrawContainerValue: App["excalidrawContainerValue"]; }; -export type PointerDownState = Readonly<{ +export type PointerDownState = { // The first position at which pointerDown happened - origin: Readonly<{ x: number; y: number }>; + origin: Readonly; // Same as "origin" but snapped to the grid, if grid is on originInGrid: Readonly<{ x: number; y: number }>; // Scrollbar checks - scrollbars: ReturnType; + scrollbars: Readonly>; // The previous pointer position - lastCoords: { x: number; y: number }; + lastCoords: GlobalPoint; // map of original elements data - originalElements: Map>; + originalElements: Readonly>>; resize: { // Handle when resizing, might change during the pointer interaction handleType: MaybeTransformHandleType; @@ -698,12 +694,12 @@ export type PointerDownState = Readonly<{ hasBeenDuplicated: boolean; hasHitCommonBoundingBoxOfSelectedElements: boolean; }; - withCmdOrCtrl: boolean; + withCmdOrCtrl: Readonly; drag: { // Might change during the pointer interaction hasOccurred: boolean; // Might change during the pointer interaction - offset: { x: number; y: number } | null; + offset: Readonly<{ x: number; y: number }> | null; }; // We need to have these in the state so that we can unsubscribe them eventListeners: { @@ -719,7 +715,7 @@ export type PointerDownState = Readonly<{ boxSelection: { hasOccurred: boolean; }; -}>; +}; export type UnsubscribeCallback = () => void; diff --git a/packages/excalidraw/utils.ts b/packages/excalidraw/utils.ts index ef8ab83082..c244238fce 100644 --- a/packages/excalidraw/utils.ts +++ b/packages/excalidraw/utils.ts @@ -1,4 +1,5 @@ -import { average } from "../math"; +import type { GlobalPoint, ViewportPoint } from "../math"; +import { average, point } from "../math"; import { COLOR_PALETTE } from "./colors"; import type { EVENT } from "./constants"; import { @@ -363,8 +364,6 @@ export const removeSelection = () => { } }; -export const distance = (x: number, y: number) => Math.abs(x - y); - export const updateActiveTool = ( appState: Pick, data: (( @@ -419,7 +418,7 @@ export const getShortcutKey = (shortcut: string): string => { }; export const viewportCoordsToSceneCoords = ( - { clientX, clientY }: { clientX: number; clientY: number }, + [clientX, clientY]: ViewportPoint, { zoom, offsetLeft, @@ -433,15 +432,15 @@ export const viewportCoordsToSceneCoords = ( scrollX: number; scrollY: number; }, -) => { +): GlobalPoint => { const x = (clientX - offsetLeft) / zoom.value - scrollX; const y = (clientY - offsetTop) / zoom.value - scrollY; - return { x, y }; + return point(x, y); }; export const sceneCoordsToViewportCoords = ( - { sceneX, sceneY }: { sceneX: number; sceneY: number }, + [sceneX, sceneY]: GlobalPoint, { zoom, offsetLeft, @@ -455,10 +454,10 @@ export const sceneCoordsToViewportCoords = ( scrollX: number; scrollY: number; }, -) => { +): ViewportPoint => { const x = (sceneX + scrollX) * zoom.value + offsetLeft; const y = (sceneY + scrollY) * zoom.value + offsetTop; - return { x, y }; + return point(x, y); }; export const getGlobalCSSVariable = (name: string) => diff --git a/packages/math/angle.ts b/packages/math/angle.ts index 2dc97a4690..21cd653d2a 100644 --- a/packages/math/angle.ts +++ b/packages/math/angle.ts @@ -4,6 +4,7 @@ import type { LocalPoint, PolarCoords, Radians, + ViewportPoint, } from "./types"; import { PRECISION } from "./utils"; @@ -23,10 +24,9 @@ export const normalizeRadians = (angle: Radians): Radians => { * (x, y) for the center point 0,0 where the first number returned is the radius, * the second is the angle in radians. */ -export const cartesian2Polar =

([ - x, - y, -]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)]; +export const cartesian2Polar = < + P extends GlobalPoint | LocalPoint | ViewportPoint, +>([x, y]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)]; export function degreesToRadians(degrees: Degrees): Radians { return ((degrees * Math.PI) / 180) as Radians; diff --git a/packages/math/arc.ts b/packages/math/arc.ts index c93830dbac..a9a3130218 100644 --- a/packages/math/arc.ts +++ b/packages/math/arc.ts @@ -1,12 +1,19 @@ import { cartesian2Polar } from "./angle"; -import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types"; +import type { + GlobalPoint, + LocalPoint, + SymmetricArc, + ViewportPoint, +} from "./types"; import { PRECISION } from "./utils"; /** * Determines if a cartesian point lies on a symmetric arc, i.e. an arc which * is part of a circle contour centered on 0, 0. */ -export const isPointOnSymmetricArc =

( +export const isPointOnSymmetricArc = < + P extends GlobalPoint | LocalPoint | ViewportPoint, +>( { radius: arcRadius, startAngle, endAngle }: SymmetricArc, point: P, ): boolean => { diff --git a/packages/math/point.ts b/packages/math/point.ts index 97b5742707..10b41bd226 100644 --- a/packages/math/point.ts +++ b/packages/math/point.ts @@ -5,6 +5,7 @@ import type { Radians, Degrees, Vector, + ViewportPoint, } from "./types"; import { PRECISION } from "./utils"; import { vectorFromPoint, vectorScale } from "./vector"; @@ -16,7 +17,7 @@ import { vectorFromPoint, vectorScale } from "./vector"; * @param y The Y coordinate * @returns The branded and created point */ -export function point( +export function point( x: number, y: number, ): Point { @@ -29,9 +30,9 @@ export function point( * @param numberArray The number array to check and to convert to Point * @returns The point instance */ -export function pointFromArray( - numberArray: number[], -): Point | undefined { +export function pointFromArray< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(numberArray: number[]): Point | undefined { return numberArray.length === 2 ? point(numberArray[0], numberArray[1]) : undefined; @@ -43,9 +44,9 @@ export function pointFromArray( * @param pair A number pair to convert to Point * @returns The point instance */ -export function pointFromPair( - pair: [number, number], -): Point { +export function pointFromPair< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(pair: [number, number]): Point { return pair as Point; } @@ -55,9 +56,9 @@ export function pointFromPair( * @param v The vector to convert * @returns The point the vector points at with origin 0,0 */ -export function pointFromVector

( - v: Vector, -): P { +export function pointFromVector< + P extends GlobalPoint | LocalPoint | ViewportPoint, +>(v: Vector): P { return v as unknown as P; } @@ -67,7 +68,9 @@ export function pointFromVector

( * @param p The value to attempt verification on * @returns TRUE if the provided value has the shape of a local or global point */ -export function isPoint(p: unknown): p is LocalPoint | GlobalPoint { +export function isPoint( + p: unknown, +): p is LocalPoint | GlobalPoint | ViewportPoint { return ( Array.isArray(p) && p.length === 2 && @@ -86,10 +89,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint { * @param b Point The second point to compare * @returns TRUE if the points are sufficiently close to each other */ -export function pointsEqual( - a: Point, - b: Point, -): boolean { +export function pointsEqual< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(a: Point, b: Point): boolean { const abs = Math.abs; return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION; } @@ -102,11 +104,9 @@ export function pointsEqual( * @param angle The radians to rotate the point by * @returns The rotated point */ -export function pointRotateRads( - [x, y]: Point, - [cx, cy]: Point, - angle: Radians, -): Point { +export function pointRotateRads< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>([x, y]: Point, [cx, cy]: Point, angle: Radians): Point { return point( (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx, (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy, @@ -121,11 +121,9 @@ export function pointRotateRads( * @param angle The degree to rotate the point by * @returns The rotated point */ -export function pointRotateDegs( - point: Point, - center: Point, - angle: Degrees, -): Point { +export function pointRotateDegs< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(point: Point, center: Point, angle: Degrees): Point { return pointRotateRads(point, center, degreesToRadians(angle)); } @@ -143,8 +141,8 @@ export function pointRotateDegs( */ // TODO 99% of use is translating between global and local coords, which need to be formalized export function pointTranslate< - From extends GlobalPoint | LocalPoint, - To extends GlobalPoint | LocalPoint, + From extends GlobalPoint | LocalPoint | ViewportPoint, + To extends GlobalPoint | LocalPoint | ViewportPoint, >(p: From, v: Vector = [0, 0] as Vector): To { return point(p[0] + v[0], p[1] + v[1]); } @@ -156,8 +154,14 @@ export function pointTranslate< * @param b The other point to create the middle point for * @returns The middle point */ -export function pointCenter

(a: P, b: P): P { - return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); +export function pointCenter

( + ...p: P[] +): P { + return pointFromPair( + p + .reduce((mid, x) => [mid[0] + x[0], mid[1] + x[1]], [0, 0]) + .map((x) => x / p.length) as [number, number], + ); } /** @@ -168,10 +172,9 @@ export function pointCenter

(a: P, b: P): P { * @param b The other point to act like the vector to translate by * @returns */ -export function pointAdd( - a: Point, - b: Point, -): Point { +export function pointAdd< + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>(a: Point, b: Point): Point { return point(a[0] + b[0], a[1] + b[1]); } @@ -183,10 +186,9 @@ export function pointAdd( * @param b The point which will act like a vector * @returns The resulting point */ -export function pointSubtract( - a: Point, - b: Point, -): Point { +export function pointSubtract< + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>(a: Point, b: Point): Point { return point(a[0] - b[0], a[1] - b[1]); } @@ -197,10 +199,9 @@ export function pointSubtract( * @param b Second point * @returns The euclidean distance between the two points. */ -export function pointDistance

( - a: P, - b: P, -): number { +export function pointDistance< + P extends LocalPoint | GlobalPoint | ViewportPoint, +>(a: P, b: P): number { return Math.hypot(b[0] - a[0], b[1] - a[1]); } @@ -213,10 +214,9 @@ export function pointDistance

( * @param b Second point * @returns The euclidean distance between the two points. */ -export function pointDistanceSq

( - a: P, - b: P, -): number { +export function pointDistanceSq< + P extends LocalPoint | GlobalPoint | ViewportPoint, +>(a: P, b: P): number { return Math.hypot(b[0] - a[0], b[1] - a[1]); } @@ -228,7 +228,9 @@ export function pointDistanceSq

( * @param multiplier The scaling factor * @returns */ -export const pointScaleFromOrigin =

( +export const pointScaleFromOrigin = < + P extends GlobalPoint | LocalPoint | ViewportPoint, +>( p: P, mid: P, multiplier: number, @@ -243,7 +245,9 @@ export const pointScaleFromOrigin =

( * @param r The other point to compare against * @returns TRUE if q is indeed between p and r */ -export const isPointWithinBounds =

( +export const isPointWithinBounds = < + P extends GlobalPoint | LocalPoint | ViewportPoint, +>( p: P, q: P, r: P, diff --git a/packages/math/polygon.ts b/packages/math/polygon.ts index 783bc4cf3e..f844ab8d0c 100644 --- a/packages/math/polygon.ts +++ b/packages/math/polygon.ts @@ -1,6 +1,6 @@ import { pointsEqual } from "./point"; import { lineSegment, pointOnLineSegment } from "./segment"; -import type { GlobalPoint, LocalPoint, Polygon } from "./types"; +import type { GlobalPoint, LocalPoint, Polygon, ViewportPoint } from "./types"; import { PRECISION } from "./utils"; export function polygon( @@ -9,13 +9,15 @@ export function polygon( return polygonClose(points) as Polygon; } -export function polygonFromPoints( - points: Point[], -) { +export function polygonFromPoints< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(points: Point[]) { return polygonClose(points) as Polygon; } -export const polygonIncludesPoint = ( +export const polygonIncludesPoint = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, polygon: Polygon, ) => { @@ -40,7 +42,9 @@ export const polygonIncludesPoint = ( return inside; }; -export const pointOnPolygon = ( +export const pointOnPolygon = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( p: Point, poly: Polygon, threshold = PRECISION, @@ -57,7 +61,7 @@ export const pointOnPolygon = ( return on; }; -function polygonClose( +function polygonClose( polygon: Point[], ) { return polygonIsClosed(polygon) @@ -65,8 +69,8 @@ function polygonClose( : ([...polygon, polygon[0]] as Polygon); } -function polygonIsClosed( - polygon: Point[], -) { +function polygonIsClosed< + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>(polygon: Point[]) { return pointsEqual(polygon[0], polygon[polygon.length - 1]); } diff --git a/packages/math/range.ts b/packages/math/range.ts index 314d1c8aef..0bef4f2636 100644 --- a/packages/math/range.ts +++ b/packages/math/range.ts @@ -71,8 +71,8 @@ export const rangeIntersection = ( * Determine if a value is inside a range. * * @param value The value to check - * @param range The range - * @returns + * @param range The range to check + * @returns TRUE if the value is in range */ export const rangeIncludesValue = ( value: number, @@ -80,3 +80,13 @@ export const rangeIncludesValue = ( ): boolean => { return value >= min && value <= max; }; + +/** + * Determine the distance between the start and end of the range. + * + * @param range The range of which to measure the extent of + * @returns The scalar distance or extent of the start and end of the range + */ +export function rangeExtent([a, b]: InclusiveRange) { + return Math.abs(a - b); +} diff --git a/packages/math/segment.ts b/packages/math/segment.ts index 6c0c2de342..3f91a1deef 100644 --- a/packages/math/segment.ts +++ b/packages/math/segment.ts @@ -4,7 +4,13 @@ import { pointFromVector, pointRotateRads, } from "./point"; -import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types"; +import type { + GlobalPoint, + LineSegment, + LocalPoint, + Radians, + ViewportPoint, +} from "./types"; import { PRECISION } from "./utils"; import { vectorAdd, @@ -20,7 +26,7 @@ import { * @param points The two points delimiting the line segment on each end * @returns The line segment delineated by the points */ -export function lineSegment

( +export function lineSegment

( a: P, b: P, ): LineSegment

{ @@ -57,7 +63,9 @@ export const isLineSegment = ( * @param origin * @returns */ -export const lineSegmentRotate = ( +export const lineSegmentRotate = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( l: LineSegment, angle: Radians, origin?: Point, @@ -72,7 +80,9 @@ export const lineSegmentRotate = ( * Calculates the point two line segments with a definite start and end point * intersect at. */ -export const segmentsIntersectAt = ( +export const segmentsIntersectAt = < + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>( a: Readonly>, b: Readonly>, ): Point | null => { @@ -105,7 +115,9 @@ export const segmentsIntersectAt = ( return null; }; -export const pointOnLineSegment = ( +export const pointOnLineSegment = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, line: LineSegment, threshold = PRECISION, @@ -119,7 +131,9 @@ export const pointOnLineSegment = ( return distance < threshold; }; -export const distanceToLineSegment = ( +export const distanceToLineSegment = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, line: LineSegment, ) => { diff --git a/packages/math/types.ts b/packages/math/types.ts index 138a44bc0b..2dce6ae978 100644 --- a/packages/math/types.ts +++ b/packages/math/types.ts @@ -43,6 +43,13 @@ export type LocalPoint = [x: number, y: number] & { _brand: "excalimath__localpoint"; }; +/** + * Represents a 2D position on the browser viewport. + */ +export type ViewportPoint = [x: number, y: number] & { + _brand: "excalimath_viewportpoint"; +}; + // Line /** @@ -57,7 +64,10 @@ export type Line

= [p: P, q: P] & { * line that is bounded by two distinct end points, and * contains every point on the line that is between its endpoints. */ -export type LineSegment

= [a: P, b: P] & { +export type LineSegment

= [ + a: P, + b: P, +] & { _brand: "excalimath_linesegment"; }; @@ -93,9 +103,10 @@ export type Triangle

= [ * A polygon is a closed shape by connecting the given points * rectangles and diamonds are modelled by polygons */ -export type Polygon = Point[] & { - _brand: "excalimath_polygon"; -}; +export type Polygon = + Point[] & { + _brand: "excalimath_polygon"; + }; // // Curve @@ -104,7 +115,7 @@ export type Polygon = Point[] & { /** * Cubic bezier curve with four control points */ -export type Curve = [ +export type Curve = [ Point, Point, Point, diff --git a/packages/math/vector.ts b/packages/math/vector.ts index 9b640b2432..94849d0e38 100644 --- a/packages/math/vector.ts +++ b/packages/math/vector.ts @@ -1,4 +1,4 @@ -import type { GlobalPoint, LocalPoint, Vector } from "./types"; +import type { GlobalPoint, LocalPoint, Vector, ViewportPoint } from "./types"; /** * Create a vector from the x and y coordiante elements. @@ -23,10 +23,9 @@ export function vector( * @param origin The origin point in a given coordiante system * @returns The created vector from the point and the origin */ -export function vectorFromPoint( - p: Point, - origin: Point = [0, 0] as Point, -): Vector { +export function vectorFromPoint< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>(p: Point, origin: Point = [0, 0] as Point): Vector { return vector(p[0] - origin[0], p[1] - origin[1]); } diff --git a/packages/utils/collision.ts b/packages/utils/collision.ts index 1269397379..f440c1a082 100644 --- a/packages/utils/collision.ts +++ b/packages/utils/collision.ts @@ -4,7 +4,7 @@ import { pointOnEllipse, type GeometricShape, } from "./geometry/shape"; -import type { Curve } from "../math"; +import type { Curve, ViewportPoint } from "../math"; import { lineSegment, point, @@ -18,7 +18,9 @@ import { } from "../math"; // check if the given point is considered on the given shape's border -export const isPointOnShape = ( +export const isPointOnShape = < + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>( point: Point, shape: GeometricShape, tolerance = 0, @@ -45,21 +47,21 @@ export const isPointOnShape = ( // check if the given point is considered inside the element's border export const isPointInShape = ( - point: Point, + p: Point, shape: GeometricShape, ) => { switch (shape.type) { case "polygon": - return polygonIncludesPoint(point, shape.data); + return polygonIncludesPoint(p, shape.data); case "line": return false; case "curve": return false; case "ellipse": - return pointInEllipse(point, shape.data); + return pointInEllipse(p, shape.data); case "polyline": { const polygon = polygonFromPoints(shape.data.flat()); - return polygonIncludesPoint(point, polygon); + return polygonIncludesPoint(p, polygon); } case "polycurve": { return false; @@ -77,7 +79,9 @@ export const isPointInBounds = ( return polygonIncludesPoint(point, bounds); }; -const pointOnPolycurve = ( +const pointOnPolycurve = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, polycurve: Polycurve, tolerance: number, @@ -85,7 +89,9 @@ const pointOnPolycurve = ( return polycurve.some((curve) => pointOnCurve(point, curve, tolerance)); }; -const cubicBezierEquation = ( +const cubicBezierEquation = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( curve: Curve, ) => { const [p0, p1, p2, p3] = curve; @@ -97,7 +103,9 @@ const cubicBezierEquation = ( p0[idx] * Math.pow(t, 3); }; -const polyLineFromCurve = ( +const polyLineFromCurve = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( curve: Curve, segments = 10, ): Polyline => { @@ -119,7 +127,9 @@ const polyLineFromCurve = ( return lineSegments; }; -export const pointOnCurve = ( +export const pointOnCurve = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, curve: Curve, threshold: number, @@ -127,7 +137,9 @@ export const pointOnCurve = ( return pointOnPolyline(point, polyLineFromCurve(curve), threshold); }; -export const pointOnPolyline = ( +export const pointOnPolyline = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, polyline: Polyline, threshold = 10e-5, diff --git a/packages/utils/geometry/geometry.test.ts b/packages/utils/geometry/geometry.test.ts index 6ee357d707..6c096074fd 100644 --- a/packages/utils/geometry/geometry.test.ts +++ b/packages/utils/geometry/geometry.test.ts @@ -11,18 +11,6 @@ import { import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape"; describe("point and line", () => { - // const l: Line = line(point(1, 0), point(1, 2)); - - // it("point on left or right of line", () => { - // expect(pointLeftofLine(point(0, 1), l)).toBe(true); - // expect(pointLeftofLine(point(1, 1), l)).toBe(false); - // expect(pointLeftofLine(point(2, 1), l)).toBe(false); - - // expect(pointRightofLine(point(0, 1), l)).toBe(false); - // expect(pointRightofLine(point(1, 1), l)).toBe(false); - // expect(pointRightofLine(point(2, 1), l)).toBe(true); - // }); - const s: LineSegment = lineSegment(point(1, 0), point(1, 2)); it("point on the line", () => { diff --git a/packages/utils/geometry/shape.ts b/packages/utils/geometry/shape.ts index f896f2e6f6..22f0ec2261 100644 --- a/packages/utils/geometry/shape.ts +++ b/packages/utils/geometry/shape.ts @@ -12,7 +12,13 @@ * to pure shapes */ -import type { Curve, LineSegment, Polygon, Radians } from "../../math"; +import type { + Curve, + LineSegment, + Polygon, + Radians, + ViewportPoint, +} from "../../math"; import { curve, lineSegment, @@ -56,24 +62,27 @@ import { invariant } from "../../excalidraw/utils"; // a polyline (made up term here) is a line consisting of other line segments // this corresponds to a straight line element in the editor but it could also // be used to model other elements -export type Polyline = +export type Polyline = LineSegment[]; // a polycurve is a curve consisting of ther curves, this corresponds to a complex // curve on the canvas -export type Polycurve = Curve[]; +export type Polycurve = + Curve[]; // an ellipse is specified by its center, angle, and its major and minor axes // but for the sake of simplicity, we've used halfWidth and halfHeight instead // in replace of semi major and semi minor axes -export type Ellipse = { +export type Ellipse = { center: Point; angle: Radians; halfWidth: number; halfHeight: number; }; -export type GeometricShape = +export type GeometricShape< + Point extends GlobalPoint | LocalPoint | ViewportPoint, +> = | { type: "line"; data: LineSegment; @@ -239,7 +248,9 @@ export const getCurveShape = ( }; }; -const polylineFromPoints = ( +const polylineFromPoints = < + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>( points: Point[], ): Polyline => { let previousPoint: Point = points[0]; @@ -254,13 +265,15 @@ const polylineFromPoints = ( return polyline; }; -export const getFreedrawShape = ( +export const getFreedrawShape = < + Point extends GlobalPoint | LocalPoint | ViewportPoint, +>( element: ExcalidrawFreeDrawElement, center: Point, isClosed: boolean = false, ): GeometricShape => { - const transform = (p: Point) => - pointRotateRads( + const transform = (p: Point): Point => + pointRotateRads( pointFromVector( vectorAdd(vectorFromPoint(p), vector(element.x, element.y)), ), @@ -391,7 +404,9 @@ export const segmentIntersectRectangleElement = < .filter((i): i is Point => !!i); }; -const distanceToEllipse = ( +const distanceToEllipse = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( p: Point, ellipse: Ellipse, ) => { @@ -445,7 +460,9 @@ const distanceToEllipse = ( return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY)); }; -export const pointOnEllipse = ( +export const pointOnEllipse = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( point: Point, ellipse: Ellipse, threshold = PRECISION, @@ -453,7 +470,9 @@ export const pointOnEllipse = ( return distanceToEllipse(point, ellipse) <= threshold; }; -export const pointInEllipse = ( +export const pointInEllipse = < + Point extends LocalPoint | GlobalPoint | ViewportPoint, +>( p: Point, ellipse: Ellipse, ) => {