diff --git a/examples/with-script-in-browser/components/ExampleApp.tsx b/examples/with-script-in-browser/components/ExampleApp.tsx index 294a11a58..ee7b4735b 100644 --- a/examples/with-script-in-browser/components/ExampleApp.tsx +++ b/examples/with-script-in-browser/components/ExampleApp.tsx @@ -424,7 +424,7 @@ export default function ExampleApp({ if (!excalidrawAPI) { return false; } - const { x, y } = viewportCoordsToSceneCoords( + const [x, y] = viewportCoordsToSceneCoords( { clientX: event.clientX - pointerDownState.hitElementOffsets.x, clientY: event.clientY - pointerDownState.hitElementOffsets.y, diff --git a/packages/common/src/points.ts b/packages/common/src/points.ts index e8f988203..0d18a5cde 100644 --- a/packages/common/src/points.ts +++ b/packages/common/src/points.ts @@ -1,14 +1,9 @@ -import { - pointFromPair, - type GlobalPoint, - type LocalPoint, -} from "@excalidraw/math"; +import { type GenericPoint, pointFromPair } from "@excalidraw/math"; +import { pointFrom } from "@excalidraw/math"; import type { NullableGridSize } from "@excalidraw/excalidraw/types"; -export const getSizeFromPoints = ( - points: readonly (GlobalPoint | LocalPoint)[], -) => { +export const getSizeFromPoints = (points: readonly GenericPoint[]) => { const xs = points.map((point) => point[0]); const ys = points.map((point) => point[1]); return { @@ -18,7 +13,7 @@ export const getSizeFromPoints = ( }; /** @arg dimension, 0 for rescaling only x, 1 for y */ -export const rescalePoints = ( +export const rescalePoints = ( dimension: 0 | 1, newSize: number, points: readonly Point[], @@ -65,16 +60,16 @@ export const rescalePoints = ( }; // TODO: Rounding this point causes some shake when free drawing -export const getGridPoint = ( +export const getGridPoint = ( x: number, y: number, gridSize: NullableGridSize, -): [number, number] => { +): Point => { if (gridSize) { - return [ + return pointFrom( Math.round(x / gridSize) * gridSize, Math.round(y / gridSize) * gridSize, - ]; + ); } - return [x, y]; + return pointFrom(x, y); }; diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index d7ccb17bf..d94e51c46 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,4 +1,10 @@ -import { average, pointFrom, type GlobalPoint } from "@excalidraw/math"; +import { + average, + pointFrom, + type ViewportPoint, + type GlobalPoint, + type GenericPoint, +} from "@excalidraw/math"; import type { ExcalidrawBindableElement, @@ -448,11 +454,11 @@ export const viewportCoordsToSceneCoords = ( scrollX: number; scrollY: number; }, -) => { +): ViewportPoint => { const x = (clientX - offsetLeft) / zoom.value - scrollX; const y = (clientY - offsetTop) / zoom.value - scrollY; - return { x, y }; + return pointFrom(x, y); }; export const sceneCoordsToViewportCoords = ( @@ -470,10 +476,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 pointFrom(x, y); }; export const getGlobalCSSVariable = (name: string) => @@ -492,11 +498,11 @@ const RE_RTL_CHECK = new RegExp(`^[^${RS_LTR_CHARS}]*[${RS_RTL_CHARS}]`); */ export const isRTL = (text: string) => RE_RTL_CHECK.test(text); -export const tupleToCoors = ( +export const tupleToCoors = ( xyTuple: readonly [number, number], -): { x: number; y: number } => { +): Point => { const [x, y] = xyTuple; - return { x, y }; + return pointFrom(x, y); }; /** use as a rejectionHandler to mute filesystem Abort errors */ diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 1d39ce2f0..1777c5819 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -29,7 +29,7 @@ import { import { isPointOnShape } from "@excalidraw/utils/collision"; -import type { LocalPoint, Radians } from "@excalidraw/math"; +import type { GenericPoint, LocalPoint, Radians } from "@excalidraw/math"; import type Scene from "@excalidraw/excalidraw/scene/Scene"; @@ -425,10 +425,10 @@ export const getSuggestedBindingsForArrows = ( ); }; -export const maybeBindLinearElement = ( +export const maybeBindLinearElement = ( linearElement: NonDeleted, appState: AppState, - pointerCoords: { x: number; y: number }, + pointerCoords: Point, elementsMap: NonDeletedSceneElementsMap, elements: readonly NonDeletedExcalidrawElement[], ): void => { @@ -576,11 +576,8 @@ const unbindLinearElement = ( return binding.elementId; }; -export const getHoveredElementForBinding = ( - pointerCoords: { - x: number; - y: number; - }, +export const getHoveredElementForBinding = ( + pointerCoords: Point, elements: readonly NonDeletedExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap, zoom?: AppState["zoom"], @@ -1392,11 +1389,11 @@ const getElligibleElementForBindingElement = ( ); }; -const getLinearElementEdgeCoors = ( +const getLinearElementEdgeCoors = ( linearElement: NonDeleted, startOrEnd: "start" | "end", elementsMap: NonDeletedSceneElementsMap, -): { x: number; y: number } => { +): Point => { const index = startOrEnd === "start" ? 0 : -1; return tupleToCoors( LinearElementEditor.getPointAtIndexGlobalCoordinates( @@ -1516,9 +1513,9 @@ const newBoundElements = ( return nextBoundElements; }; -export const bindingBorderTest = ( +export const bindingBorderTest = ( element: NonDeleted, - { x, y }: { x: number; y: number }, + [x, y]: Point, elementsMap: NonDeletedSceneElementsMap, zoom?: AppState["zoom"], fullShape?: boolean, diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index d0c071f2c..ede345501 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -23,6 +23,7 @@ import { pointsOnBezierCurves } from "points-on-curve"; import type { Curve, Degrees, + GenericPoint, GlobalPoint, LineSegment, LocalPoint, @@ -1057,7 +1058,7 @@ export const getElementPointsCoords = ( export const getClosestElementBounds = ( elements: readonly ExcalidrawElement[], - from: { x: number; y: number }, + from: GenericPoint, ): Bounds => { if (!elements.length) { return [0, 0, 0, 0]; @@ -1070,7 +1071,7 @@ export const getClosestElementBounds = ( const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); const distance = pointDistance( pointFrom((x1 + x2) / 2, (y1 + y2) / 2), - pointFrom(from.x, from.y), + from, ); if (distance < minDistance) { diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 07b17bfde..c9d75d05d 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -19,9 +19,9 @@ import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape"; import type { + GenericPoint, GlobalPoint, LineSegment, - LocalPoint, Polygon, Radians, } from "@excalidraw/math"; @@ -72,7 +72,7 @@ export const shouldTestInside = (element: ExcalidrawElement) => { return isDraggableFromInside || isImageElement(element); }; -export type HitTestArgs = { +export type HitTestArgs = { x: number; y: number; element: ExcalidrawElement; @@ -81,7 +81,7 @@ export type HitTestArgs = { frameNameBound?: FrameNameBounds | null; }; -export const hitElementItself = ({ +export const hitElementItself = ({ x, y, element, @@ -127,9 +127,7 @@ export const hitElementBoundingBox = ( ); }; -export const hitElementBoundingBoxOnly = < - Point extends GlobalPoint | LocalPoint, ->( +export const hitElementBoundingBoxOnly = ( hitArgs: HitTestArgs, elementsMap: ElementsMap, ) => { @@ -145,7 +143,7 @@ export const hitElementBoundingBoxOnly = < ); }; -export const hitElementBoundText = ( +export const hitElementBoundText = ( x: number, y: number, textShape: GeometricShape | null, diff --git a/packages/element/src/dragElements.ts b/packages/element/src/dragElements.ts index 669417a54..5d1efc986 100644 --- a/packages/element/src/dragElements.ts +++ b/packages/element/src/dragElements.ts @@ -12,6 +12,7 @@ import type { } from "@excalidraw/excalidraw/types"; import type Scene from "@excalidraw/excalidraw/scene/Scene"; +import type { GenericPoint } from "@excalidraw/math"; import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; @@ -208,10 +209,7 @@ export const dragNewElement = ({ /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is true */ widthAspectRatio?: number | null; - originOffset?: { - x: number; - y: number; - } | null; + originOffset?: GenericPoint | null; informMutation?: boolean; }) => { if (shouldMaintainAspectRatio && newElement.type !== "selection") { @@ -285,11 +283,12 @@ export const dragNewElement = ({ }; } + const [originOffsetX, originOffsetY] = originOffset ?? [0, 0]; mutateElement( newElement, { - x: newX + (originOffset?.x ?? 0), - y: newY + (originOffset?.y ?? 0), + x: newX + originOffsetX, + y: newY + originOffsetY, width, height, ...textAutoResize, diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index a70e265bc..4461d1e9d 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -9,6 +9,7 @@ import { vectorCross, vectorFromPoint, vectorScale, + type GenericPoint, type GlobalPoint, type LocalPoint, } from "@excalidraw/math"; @@ -1642,7 +1643,7 @@ const pathTo = (start: Node, node: Node) => { return path; }; -const m_dist = (a: GlobalPoint | LocalPoint, b: GlobalPoint | LocalPoint) => +const m_dist = (a: GenericPoint, b: GenericPoint) => Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); /** @@ -2291,7 +2292,7 @@ const getHoveredElement = ( const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean => a[0] === b[0] && a[1] === b[1]; -export const validateElbowPoints =

( +export const validateElbowPoints =

( points: readonly P[], tolerance: number = DEDUP_TRESHOLD, ) => diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 664c8d98f..53390d770 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -1,5 +1,9 @@ import { arrayToMap } from "@excalidraw/common"; -import { isPointWithinBounds, pointFrom } from "@excalidraw/math"; +import { + type GenericPoint, + isPointWithinBounds, + pointFrom, +} from "@excalidraw/math"; import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox"; import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds"; @@ -152,11 +156,8 @@ export const elementOverlapsWithFrame = ( ); }; -export const isCursorInFrame = ( - cursorCoords: { - x: number; - y: number; - }, +export const isCursorInFrame = ( + cursorCoords: Point, frame: NonDeleted, elementsMap: ElementsMap, ) => { @@ -164,7 +165,7 @@ export const isCursorInFrame = ( return isPointWithinBounds( pointFrom(fx1, fy1), - pointFrom(cursorCoords.x, cursorCoords.y), + cursorCoords, pointFrom(fx2, fy2), ); }; diff --git a/packages/element/src/heading.ts b/packages/element/src/heading.ts index 1e9ab3713..6e50d0dff 100644 --- a/packages/element/src/heading.ts +++ b/packages/element/src/heading.ts @@ -13,10 +13,10 @@ import { } from "@excalidraw/math"; import type { - LocalPoint, GlobalPoint, Triangle, Vector, + GenericPoint, } from "@excalidraw/math"; import { getCenterForBounds, type Bounds } from "./bounds"; @@ -43,12 +43,10 @@ export const vectorToHeading = (vec: Vector): Heading => { return HEADING_UP; }; -export const headingForPoint =

( - p: P, - o: P, -) => vectorToHeading(vectorFromPoint

(p, o)); +export const headingForPoint =

(p: P, o: P) => + vectorToHeading(vectorFromPoint

(p, o)); -export const headingForPointIsHorizontal =

( +export const headingForPointIsHorizontal =

( p: P, o: P, ) => headingIsHorizontal(headingForPoint

(p, o)); diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 8a9117bf8..5f28801d0 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -3,8 +3,6 @@ import { pointFrom, pointRotateRads, pointsEqual, - type GlobalPoint, - type LocalPoint, pointDistance, vectorFromPoint, } from "@excalidraw/math"; @@ -26,7 +24,12 @@ import Scene from "@excalidraw/excalidraw/scene/Scene"; import type { Store } from "@excalidraw/excalidraw/store"; -import type { Radians } from "@excalidraw/math"; +import type { + GlobalPoint, + LocalPoint, + GenericPoint, + Radians, +} from "@excalidraw/math"; import type { AppState, @@ -106,7 +109,7 @@ export class LinearElementEditor { /** index */ lastClickedPoint: number; lastClickedIsEndPoint: boolean; - origin: Readonly<{ x: number; y: number }> | null; + origin: Readonly | null; segmentMidpoint: { value: GlobalPoint | null; index: number | null; @@ -117,7 +120,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: Readonly; public readonly startBindingElement: | ExcalidrawBindableElement | null @@ -139,7 +142,7 @@ export class LinearElementEditor { this.selectedPointsIndices = null; this.lastUncommittedPoint = null; this.isDragging = false; - this.pointerOffset = { x: 0, y: 0 }; + this.pointerOffset = pointFrom(0, 0); this.startBindingElement = "keep"; this.endBindingElement = "keep"; this.pointerDownState = { @@ -242,14 +245,14 @@ export class LinearElementEditor { /** * @returns whether point was dragged */ - static handlePointDragging( + static handlePointDragging( event: PointerEvent, app: AppClassProperties, scenePointerX: number, scenePointerY: number, maybeSuggestBinding: ( element: NonDeleted, - pointSceneCoords: { x: number; y: number }[], + pointSceneCoords: Point[], ) => void, linearElementEditor: LinearElementEditor, scene: Scene, @@ -320,11 +323,13 @@ export class LinearElementEditor { }, ]); } else { + const [pointerOffsetX, pointerOffsetY] = + linearElementEditor.pointerOffset; const newDraggingPointPosition = LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, + scenePointerX - pointerOffsetX, + scenePointerY - pointerOffsetY, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -339,8 +344,8 @@ export class LinearElementEditor { ? LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, + scenePointerX - pointerOffsetX, + scenePointerY - pointerOffsetY, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ) : pointFrom( @@ -363,7 +368,7 @@ export class LinearElementEditor { // suggest bindings for first and last point if selected if (isBindingElement(element, false)) { - const coords: { x: number; y: number }[] = []; + const coords: Point[] = []; const firstSelectedIndex = selectedPointsIndices[0]; if (firstSelectedIndex === 0) { @@ -511,7 +516,7 @@ export class LinearElementEditor { ? [pointerDownState.lastClickedPoint] : selectedPointsIndices, isDragging: false, - pointerOffset: { x: 0, y: 0 }, + pointerOffset: pointFrom(0, 0), }; } @@ -586,9 +591,9 @@ export class LinearElementEditor { editorMidPointsCache.zoom = appState.zoom.value; }; - static getSegmentMidpointHitCoords = ( + static getSegmentMidpointHitCoords = ( linearElementEditor: LinearElementEditor, - scenePointer: { x: number; y: number }, + [scenePointerX, scenePointerY]: Point, appState: AppState, elementsMap: ElementsMap, ): GlobalPoint | null => { @@ -601,8 +606,8 @@ export class LinearElementEditor { element, elementsMap, appState.zoom, - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ); if (!isElbowArrow(element) && clickedPointIndex >= 0) { return null; @@ -630,7 +635,7 @@ export class LinearElementEditor { existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[1], ), - pointFrom(scenePointer.x, scenePointer.y), + pointFrom(scenePointerX, scenePointerY), ); if (distance <= threshold) { return existingSegmentMidpointHitCoords; @@ -644,7 +649,7 @@ export class LinearElementEditor { if (midPoints[index] !== null) { const distance = pointDistance( midPoints[index]!, - pointFrom(scenePointer.x, scenePointer.y), + pointFrom(scenePointerX, scenePointerY), ); if (distance <= threshold) { return midPoints[index]; @@ -656,7 +661,7 @@ export class LinearElementEditor { return null; }; - static isSegmentTooShort

( + static isSegmentTooShort

( element: NonDeleted, startPoint: P, endPoint: P, @@ -747,11 +752,11 @@ export class LinearElementEditor { return -1; } - static handlePointerDown( + static handlePointerDown( event: React.PointerEvent, app: AppClassProperties, store: Store, - scenePointer: { x: number; y: number }, + scenePointer: Point, linearElementEditor: LinearElementEditor, scene: Scene, ): { @@ -762,6 +767,7 @@ export class LinearElementEditor { const appState = app.state; const elementsMap = scene.getNonDeletedElementsMap(); const elements = scene.getNonDeletedElements(); + const [scenePointerX, scenePointerY] = scenePointer; const ret: ReturnType = { didAddPoint: false, @@ -801,8 +807,8 @@ export class LinearElementEditor { LinearElementEditor.createPointAt( element, elementsMap, - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ), ], @@ -816,7 +822,7 @@ export class LinearElementEditor { prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, lastClickedPoint: -1, lastClickedIsEndPoint: false, - origin: { x: scenePointer.x, y: scenePointer.y }, + origin: pointFrom(scenePointerX, scenePointerY), segmentMidpoint: { value: segmentMidpoint, index: segmentMidpointIndex, @@ -842,8 +848,8 @@ export class LinearElementEditor { element, elementsMap, appState.zoom, - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ); // if we clicked on a point, set the element as hitElement otherwise // it would get deselected if the point is outside the hitbox area @@ -897,7 +903,7 @@ export class LinearElementEditor { prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, lastClickedPoint: clickedPointIndex, lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1, - origin: { x: scenePointer.x, y: scenePointer.y }, + origin: pointFrom(scenePointerX, scenePointerY), segmentMidpoint: { value: segmentMidpoint, index: segmentMidpointIndex, @@ -906,17 +912,17 @@ export class LinearElementEditor { }, selectedPointsIndices: nextSelectedPointsIndices, pointerOffset: targetPoint - ? { - x: scenePointer.x - targetPoint[0], - y: scenePointer.y - targetPoint[1], - } - : { x: 0, y: 0 }, + ? pointFrom( + scenePointerX - targetPoint[0], + scenePointerY - targetPoint[1], + ) + : pointFrom(0, 0), }; return ret; } - static arePointsEqual( + static arePointsEqual( point1: Point | null, point2: Point | null, ) { @@ -977,11 +983,13 @@ export class LinearElementEditor { height + lastCommittedPoint[1], ); } else { + const [pointerOffsetX, pointerOffsetY] = + appState.editingLinearElement.pointerOffset; newPoint = LinearElementEditor.createPointAt( element, elementsMap, - scenePointerX - appState.editingLinearElement.pointerOffset.x, - scenePointerY - appState.editingLinearElement.pointerOffset.y, + scenePointerX - pointerOffsetX, + scenePointerY - pointerOffsetY, event[KEYS.CTRL_OR_CMD] || isElbowArrow(element) ? null : app.getEffectiveGridSize(), @@ -1376,10 +1384,7 @@ export class LinearElementEditor { } const origin = linearElementEditor.pointerDownState.origin!; - const dist = pointDistance( - pointFrom(origin.x, origin.y), - pointFrom(pointerCoords.x, pointerCoords.y), - ); + const dist = pointDistance(origin, pointerCoords); if ( !appState.editingLinearElement && dist < DRAGGING_THRESHOLD / appState.zoom.value @@ -1412,11 +1417,12 @@ export class LinearElementEditor { selectedPointsIndices: linearElementEditor.selectedPointsIndices, }; + const [pointerX, pointerY] = pointerCoords; const midpoint = LinearElementEditor.createPointAt( element, elementsMap, - pointerCoords.x, - pointerCoords.y, + pointerX, + pointerY, snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null, ); const points = [ @@ -1528,31 +1534,28 @@ export class LinearElementEditor { element: NonDeleted, elementsMap: ElementsMap, referencePoint: LocalPoint, - scenePointer: GlobalPoint, + [scenePointerX, scenePointerY]: GlobalPoint, gridSize: NullableGridSize, ) { - const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( - element, - referencePoint, - elementsMap, - ); + const [referencePointCoordsX, referencePointCoordsY] = + LinearElementEditor.getPointGlobalCoordinates( + element, + referencePoint, + elementsMap, + ); if (isElbowArrow(element)) { return [ - scenePointer[0] - referencePointCoords[0], - scenePointer[1] - referencePointCoords[1], + scenePointerX - referencePointCoordsX, + scenePointerY - referencePointCoordsY, ]; } - const [gridX, gridY] = getGridPoint( - scenePointer[0], - scenePointer[1], - gridSize, - ); + const [gridX, gridY] = getGridPoint(scenePointerX, scenePointerY, gridSize); const { width, height } = getLockedLinearCursorAlignSize( - referencePointCoords[0], - referencePointCoords[1], + referencePointCoordsX, + referencePointCoordsY, gridX, gridY, ); diff --git a/packages/element/src/resizeTest.ts b/packages/element/src/resizeTest.ts index 411dcf9a7..219265d36 100644 --- a/packages/element/src/resizeTest.ts +++ b/packages/element/src/resizeTest.ts @@ -7,7 +7,7 @@ import { import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common"; -import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math"; +import type { GenericPoint, LineSegment } from "@excalidraw/math"; import type { AppState, Device, Zoom } from "@excalidraw/excalidraw/types"; @@ -43,7 +43,7 @@ const isInsideTransformHandle = ( y >= transformHandle[1] && y <= transformHandle[1] + transformHandle[3]; -export const resizeTest = ( +export const resizeTest = ( element: NonDeletedExcalidrawElement, elementsMap: ElementsMap, appState: AppState, @@ -152,9 +152,7 @@ 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, @@ -271,7 +269,7 @@ export const getCursorForResizingElement = (resizingElement: { return cursor ? `${cursor}-resize` : ""; }; -const getSelectionBorders = ( +const getSelectionBorders = ( [x1, y1]: Point, [x2, y2]: Point, center: Point, diff --git a/packages/element/src/shapes.ts b/packages/element/src/shapes.ts index 96542c538..4ae2f87d3 100644 --- a/packages/element/src/shapes.ts +++ b/packages/element/src/shapes.ts @@ -13,8 +13,7 @@ import { pointFromPair, pointRotateRads, pointsEqual, - type GlobalPoint, - type LocalPoint, + type GenericPoint, } from "@excalidraw/math"; import { getClosedCurveShape, @@ -46,7 +45,7 @@ import type { * get the pure geometric shape of an excalidraw elementw * which is then used for hit detection */ -export const getElementShape = ( +export const getElementShape = ( element: ExcalidrawElement, elementsMap: ElementsMap, ): GeometricShape => { @@ -98,7 +97,7 @@ export const getElementShape = ( } }; -export const getBoundTextShape = ( +export const getBoundTextShape = ( element: ExcalidrawElement, elementsMap: ElementsMap, ): GeometricShape | null => { @@ -126,9 +125,7 @@ export const getBoundTextShape = ( return null; }; -export const getControlPointsForBezierCurve = < - P extends GlobalPoint | LocalPoint, ->( +export const getControlPointsForBezierCurve =

( element: NonDeleted, endPoint: P, ) => { @@ -170,7 +167,7 @@ export const getControlPointsForBezierCurve = < return controlPoints; }; -export const getBezierXY =

( +export const getBezierXY =

( p0: P, p1: P, p2: P, @@ -187,7 +184,7 @@ export const getBezierXY =

( return pointFrom(tx, ty); }; -const getPointsInBezierCurve =

( +const getPointsInBezierCurve =

( element: NonDeleted, endPoint: P, ) => { @@ -217,7 +214,7 @@ const getPointsInBezierCurve =

( return pointsOnCurve; }; -const getBezierCurveArcLengths =

( +const getBezierCurveArcLengths =

( element: NonDeleted, endPoint: P, ) => { @@ -236,7 +233,7 @@ const getBezierCurveArcLengths =

( return arcLengths; }; -export const getBezierCurveLength =

( +export const getBezierCurveLength =

( element: NonDeleted, endPoint: P, ) => { @@ -245,7 +242,7 @@ export const getBezierCurveLength =

( }; // This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length -export const mapIntervalToBezierT =

( +export const mapIntervalToBezierT =

( element: NonDeleted, endPoint: P, interval: number, // The interval between 0 to 1 for which you want to find the point on the curve, @@ -340,7 +337,7 @@ export const aabbForElement = ( return bounds; }; -export const pointInsideBounds =

( +export const pointInsideBounds =

( p: P, bounds: Bounds, ): boolean => diff --git a/packages/element/src/sizeHelpers.ts b/packages/element/src/sizeHelpers.ts index 7a84dadba..7a9e3ba51 100644 --- a/packages/element/src/sizeHelpers.ts +++ b/packages/element/src/sizeHelpers.ts @@ -37,26 +37,28 @@ export const isElementInViewport = ( elementsMap: ElementsMap, ) => { const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates - const topLeftSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft, - clientY: viewTransformations.offsetTop, - }, - viewTransformations, - ); - const bottomRightSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + width, - clientY: viewTransformations.offsetTop + height, - }, - viewTransformations, - ); + const [topLeftSceneCoordsX, topLeftSceneCoordsY] = + viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft, + clientY: viewTransformations.offsetTop, + }, + viewTransformations, + ); + const [bottomRightSceneCoordsX, bottomRightSceneCoordsY] = + viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft + width, + clientY: viewTransformations.offsetTop + height, + }, + viewTransformations, + ); return ( - topLeftSceneCoords.x <= x2 && - topLeftSceneCoords.y <= y2 && - bottomRightSceneCoords.x >= x1 && - bottomRightSceneCoords.y >= y1 + topLeftSceneCoordsX <= x2 && + topLeftSceneCoordsY <= y2 && + bottomRightSceneCoordsX >= x1 && + bottomRightSceneCoordsY >= y1 ); }; @@ -75,26 +77,29 @@ export const isElementCompletelyInViewport = ( padding?: Offsets, ) => { 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), - }, - viewTransformations, - ); - const bottomRightSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + width - (padding?.right || 0), - clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0), - }, - viewTransformations, - ); + const [topLeftSceneCoordsX, topLeftSceneCoordsY] = + viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft + (padding?.left || 0), + clientY: viewTransformations.offsetTop + (padding?.top || 0), + }, + viewTransformations, + ); + const [bottomRightSceneCoordsX, bottomRightSceneCoordsY] = + viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft + width - (padding?.right || 0), + clientY: + viewTransformations.offsetTop + height - (padding?.bottom || 0), + }, + viewTransformations, + ); return ( - x1 >= topLeftSceneCoords.x && - y1 >= topLeftSceneCoords.y && - x2 <= bottomRightSceneCoords.x && - y2 <= bottomRightSceneCoords.y + x1 >= topLeftSceneCoordsX && + y1 >= topLeftSceneCoordsY && + x2 <= bottomRightSceneCoordsX && + y2 <= bottomRightSceneCoordsY ); }; diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index a8bd56e82..1caa9a473 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -1,4 +1,4 @@ -import { clamp, roundToStep } from "@excalidraw/math"; +import { clamp, pointFrom, roundToStep } from "@excalidraw/math"; import { DEFAULT_CANVAS_BACKGROUND_PICKS, @@ -333,7 +333,7 @@ export const zoomToFitBounds = ({ ); const centerScroll = centerScrollOn({ - scenePoint: { x: centerX, y: centerY }, + scenePoint: pointFrom(centerX, centerY), viewportDimensions: { width: appState.width, height: appState.height, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 984961656..b39cd0f11 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -143,7 +143,7 @@ export const actionFinalize = register({ maybeBindLinearElement( multiPointElement, appState, - { x, y }, + pointFrom(x, y), elementsMap, elements, ); diff --git a/packages/excalidraw/animated-trail.ts b/packages/excalidraw/animated-trail.ts index af6162e99..66fa54b98 100644 --- a/packages/excalidraw/animated-trail.ts +++ b/packages/excalidraw/animated-trail.ts @@ -182,12 +182,12 @@ export class AnimatedTrail implements Trail { const _stroke = trail .getStrokeOutline(trail.options.size / state.zoom.value) .map(([x, y]) => { - const result = sceneCoordsToViewportCoords( + const [resultX, resultY] = sceneCoordsToViewportCoords( { sceneX: x, sceneY: y }, state, ); - return [result.x, result.y]; + return [resultX, resultY]; }); const stroke = this.trailAnimation diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index a75745f2a..f2cf8600a 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -12,6 +12,8 @@ import { DEFAULT_GRID_STEP, } from "@excalidraw/common"; +import { pointFrom } from "@excalidraw/math"; + import type { AppState, NormalizedZoomValue } from "./types"; const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio) @@ -112,10 +114,7 @@ export const getDefaultAppState = (): Omit< showHyperlinkPopup: false, selectedLinearElement: null, snapLines: [], - originSnapOffset: { - x: 0, - y: 0, - }, + originSnapOffset: pointFrom(0, 0), objectsSnapModeEnabled: false, userToFollow: null, followedBy: new Set(), diff --git a/packages/excalidraw/clients.ts b/packages/excalidraw/clients.ts index 9467b1362..339a9d751 100644 --- a/packages/excalidraw/clients.ts +++ b/packages/excalidraw/clients.ts @@ -69,7 +69,7 @@ export const renderRemoteCursors = ({ }) => { // Paint remote pointers for (const [socketId, pointer] of renderConfig.remotePointerViewportCoords) { - let { x, y } = pointer; + let [x, y] = pointer; const collaborator = appState.collaborators.get(socketId); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index a70cb9808..aedd97f63 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -303,7 +303,12 @@ import { import { isNonDeletedElement } from "@excalidraw/element"; -import type { LocalPoint, Radians } from "@excalidraw/math"; +import type { + GenericPoint, + LocalPoint, + Radians, + ViewportPoint, +} from "@excalidraw/math"; import type { ExcalidrawBindableElement, @@ -391,7 +396,7 @@ import { copyTextToSystemClipboard, parseClipboard } from "../clipboard"; import { exportCanvas, loadFromBlob } from "../data"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import { restore, restoreElements } from "../data/restore"; -import { getCenter, getDistance } from "../gesture"; +import { getCenter, getDistance, isTwoPointerCoords } from "../gesture"; import { History } from "../history"; import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n"; @@ -671,8 +676,8 @@ class App extends React.Component { lastPointerUpEvent: React.PointerEvent | PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null; - lastPointerMoveCoords: { x: number; y: number } | null = null; - lastViewportPosition = { x: 0, y: 0 }; + lastPointerMoveCoords: GenericPoint | null = null; + lastViewportPosition = pointFrom(0, 0); animationFrameHandler = new AnimationFrameHandler(); @@ -1062,7 +1067,7 @@ class App extends React.Component { return ( <> {embeddableElements.map((el) => { - const { x, y } = sceneCoordsToViewportCoords( + const [x, y] = sceneCoordsToViewportCoords( { sceneX: el.x, sceneY: el.y }, this.state, ); @@ -1351,20 +1356,22 @@ class App extends React.Component { if (frameNameDiv) { const box = frameNameDiv.getBoundingClientRect(); - const boxSceneTopLeft = viewportCoordsToSceneCoords( - { clientX: box.x, clientY: box.y }, - this.state, - ); - const boxSceneBottomRight = viewportCoordsToSceneCoords( - { clientX: box.right, clientY: box.bottom }, - this.state, - ); + const [boxSceneTopLeftX, boxSceneTopLeftY] = + viewportCoordsToSceneCoords( + { clientX: box.x, clientY: box.y }, + this.state, + ); + const [boxSceneBottomRightX, boxSceneBottomRightY] = + viewportCoordsToSceneCoords( + { clientX: box.right, clientY: box.bottom }, + this.state, + ); bounds = { - x: boxSceneTopLeft.x, - y: boxSceneTopLeft.y, - width: boxSceneBottomRight.x - boxSceneTopLeft.x, - height: boxSceneBottomRight.y - boxSceneTopLeft.y, + x: boxSceneTopLeftX, + y: boxSceneTopLeftY, + width: boxSceneBottomRightX - boxSceneTopLeftX, + height: boxSceneBottomRightY - boxSceneTopLeftY, angle: 0, zoom: this.state.zoom.value, versionNonce: frameElement.versionNonce, @@ -1425,7 +1432,7 @@ class App extends React.Component { return null; } - const { x: x1, y: y1 } = sceneCoordsToViewportCoords( + const [x1, y1] = sceneCoordsToViewportCoords( { sceneX: f.x, sceneY: f.y }, this.state, ); @@ -3058,9 +3065,11 @@ class App extends React.Component { return; } + const [lastViewportPositionX, lastViewportPositionY] = + this.lastViewportPosition; const elementUnderCursor = document.elementFromPoint( - this.lastViewportPosition.x, - this.lastViewportPosition.y, + lastViewportPositionX, + lastViewportPositionY, ); if ( event && @@ -3070,10 +3079,10 @@ class App extends React.Component { return; } - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( + const [sceneX, sceneY] = viewportCoordsToSceneCoords( { - clientX: this.lastViewportPosition.x, - clientY: this.lastViewportPosition.y, + clientX: lastViewportPositionX, + clientY: lastViewportPositionY, }, this.state, ); @@ -3247,21 +3256,23 @@ class App extends React.Component { const elementsCenterX = distance(minX, maxX) / 2; const elementsCenterY = distance(minY, maxY) / 2; + const [lastViewportPositionX, lastViewportPositionY] = + this.lastViewportPosition; const clientX = typeof opts.position === "object" ? opts.position.clientX : opts.position === "cursor" - ? this.lastViewportPosition.x + ? lastViewportPositionX : this.state.width / 2 + this.state.offsetLeft; const clientY = typeof opts.position === "object" ? opts.position.clientY : opts.position === "cursor" - ? this.lastViewportPosition.y + ? lastViewportPositionY : this.state.height / 2 + this.state.offsetTop; - const { x, y } = viewportCoordsToSceneCoords( + const [x, y] = viewportCoordsToSceneCoords( { clientX, clientY }, this.state, ); @@ -3294,7 +3305,7 @@ class App extends React.Component { syncMovedIndices(nextElements, arrayToMap(duplicatedElements)); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(pointFrom(x, y)); if (topLayerFrame) { const eligibleElements = filterElementsEligibleAsFrameChildren( @@ -3476,10 +3487,12 @@ class App extends React.Component { } private addTextFromPaste(text: string, isPlainPaste = false) { - const { x, y } = viewportCoordsToSceneCoords( + const [lastViewportPositionX, lastViewportPositionY] = + this.lastViewportPosition; + const [x, y] = viewportCoordsToSceneCoords( { - clientX: this.lastViewportPosition.x, - clientY: this.lastViewportPosition.y, + clientX: lastViewportPositionX, + clientY: lastViewportPositionY, }, this.state, ); @@ -3518,10 +3531,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( + pointFrom(x, currentY), + ); let metrics = measureText(originalText, fontString, lineHeight); const isTextUnwrapped = metrics.width > maxTextWidth; @@ -4046,8 +4058,10 @@ class App extends React.Component { private updateCurrentCursorPosition = withBatchedUpdates( (event: MouseEvent) => { - this.lastViewportPosition.x = event.clientX; - this.lastViewportPosition.y = event.clientY; + this.lastViewportPosition = pointFrom( + event.clientX, + event.clientY, + ); }, ); @@ -4875,13 +4889,15 @@ class App extends React.Component { return; } + const [lastViewportPositionX, lastViewportPositionY] = + this.lastViewportPosition; const initialScale = gesture.initialScale; if (initialScale) { this.setState((state) => ({ ...getStateForZoom( { - viewportX: this.lastViewportPosition.x, - viewportY: this.lastViewportPosition.y, + viewportX: lastViewportPositionX, + viewportY: lastViewportPositionY, nextZoom: getNormalizedZoom(initialScale * event.scale), }, state, @@ -4942,7 +4958,7 @@ class App extends React.Component { id: element.id, canvas: this.canvas, getViewportCoords: (x, y) => { - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( + const [viewportX, viewportY] = sceneCoordsToViewportCoords( { sceneX: x, sceneY: y, @@ -5130,7 +5146,7 @@ class App extends React.Component { return containingFrame && this.state.frameRendering.enabled && this.state.frameRendering.clip - ? isCursorInFrame({ x, y }, containingFrame, elementsMap) + ? isCursorInFrame(pointFrom(x, y), containingFrame, elementsMap) : true; }) .filter((el) => { @@ -5333,10 +5349,9 @@ class App extends React.Component { } } - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: sceneX, - y: sceneY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords( + pointFrom(sceneX, sceneY), + ); const element = existingTextElement || @@ -5432,10 +5447,8 @@ class App extends React.Component { const selectedElements = this.scene.getSelectedElements(this.state); - let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( - event, - this.state, - ); + const sceneCoords = viewportCoordsToSceneCoords(event, this.state); + let [sceneX, sceneY] = sceneCoords; if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if ( @@ -5456,7 +5469,7 @@ class App extends React.Component { ) { const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords( this.state.selectedLinearElement, - { x: sceneX, y: sceneY }, + sceneCoords, this.state, this.scene.getNonDeletedElementsMap(), ); @@ -5478,7 +5491,7 @@ class App extends React.Component { ...this.state.selectedLinearElement, segmentMidPointHoveredCoords: null, }, - { x: sceneX, y: sceneY }, + pointFrom(screenX, screenY), this.state, this.scene.getNonDeletedElementsMap(), ); @@ -5593,10 +5606,11 @@ class App extends React.Component { } }; - private getElementLinkAtPosition = ( - scenePointer: Readonly<{ x: number; y: number }>, + private getElementLinkAtPosition = ( + scenePointer: Readonly, hitElement: NonDeletedExcalidrawElement | null, ): ExcalidrawElement | undefined => { + const [scenePointerX, scenePointerY] = scenePointer; const elements = this.scene.getNonDeletedElements(); let hitElementIndex = -1; @@ -5612,7 +5626,7 @@ class App extends React.Component { element, this.scene.getNonDeletedElementsMap(), this.state, - pointFrom(scenePointer.x, scenePointer.y), + pointFrom(scenePointerX, scenePointerY), this.device.editor.isMobile, ) ) { @@ -5638,27 +5652,23 @@ class App extends React.Component { if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD) { return; } - const lastPointerDownCoords = viewportCoordsToSceneCoords( - this.lastPointerDownEvent!, - this.state, - ); + const [lastPointerDownCoordsX, lastPointerDownCoordsY] = + viewportCoordsToSceneCoords(this.lastPointerDownEvent!, this.state); const elementsMap = this.scene.getNonDeletedElementsMap(); const lastPointerDownHittingLinkIcon = isPointHittingLink( this.hitLinkElement, elementsMap, this.state, - pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y), + pointFrom(lastPointerDownCoordsX, lastPointerDownCoordsY), this.device.editor.isMobile, ); - const lastPointerUpCoords = viewportCoordsToSceneCoords( - this.lastPointerUpEvent!, - this.state, - ); + const [lastPointerUpCoordsX, lastPointerUpCoordsY] = + viewportCoordsToSceneCoords(this.lastPointerUpEvent!, this.state); const lastPointerUpHittingLinkIcon = isPointHittingLink( this.hitLinkElement, elementsMap, this.state, - pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y), + pointFrom(lastPointerUpCoordsX, lastPointerUpCoordsY), this.device.editor.isMobile, ); if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { @@ -5690,10 +5700,9 @@ class App extends React.Component { } }; - private getTopLayerFrameAtSceneCoords = (sceneCoords: { - x: number; - y: number; - }) => { + private getTopLayerFrameAtSceneCoords = ( + sceneCoords: Point, + ) => { const elementsMap = this.scene.getNonDeletedElementsMap(); const frames = this.scene .getNonDeletedFramesLikes() @@ -5711,10 +5720,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, + pointFrom(event.clientX, event.clientY), + ); } const initialScale = gesture.initialScale; @@ -5725,11 +5734,17 @@ class App extends React.Component { gesture.initialDistance ) { const center = getCenter(gesture.pointers); - const deltaX = center.x - gesture.lastCenter.x; - const deltaY = center.y - gesture.lastCenter.y; + const [centerX, centerY] = center; + const [lastCenterX, lastCenterY] = gesture.lastCenter; + + const deltaX = centerX - lastCenterX; + const deltaY = centerY - lastCenterY; gesture.lastCenter = center; - const distance = getDistance(Array.from(gesture.pointers.values())); + const gesturePointers = Array.from(gesture.pointers.values()); + const distance = isTwoPointerCoords(gesturePointers) + ? getDistance(gesturePointers) + : 0; const scaleFactor = this.state.activeTool.type === "freedraw" && this.state.penMode ? 1 @@ -5742,8 +5757,8 @@ class App extends React.Component { this.setState((state) => { const zoomState = getStateForZoom( { - viewportX: center.x, - viewportY: center.y, + viewportX: centerX, + viewportY: centerY, nextZoom, }, state, @@ -5796,7 +5811,7 @@ class App extends React.Component { } const scenePointer = viewportCoordsToSceneCoords(event, this.state); - const { x: scenePointerX, y: scenePointerY } = scenePointer; + const [scenePointerX, scenePointerY] = scenePointer; if ( !this.state.newElement && @@ -6084,10 +6099,7 @@ class App extends React.Component { } } - const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, - ); + const hitElement = this.getElementAtPosition(scenePointerX, scenePointerY); this.hitLinkElement = this.getElementLinkAtPosition( scenePointer, @@ -6203,13 +6215,14 @@ class App extends React.Component { } }; - private handleEraser = ( + private handleEraser = ( event: PointerEvent, - scenePointer: { x: number; y: number }, + scenePointer: Point, ) => { + const [scenePointerX, scenePointerY] = scenePointer; const elementsToErase = this.eraserTrail.addPointToPath( - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, event.altKey, ); @@ -6261,7 +6274,7 @@ class App extends React.Component { segmentMidPointHoveredCoords = LinearElementEditor.getSegmentMidpointHitCoords( linearElementEditor, - { x: scenePointerX, y: scenePointerY }, + pointFrom(scenePointerX, scenePointerY), this.state, this.scene.getNonDeletedElementsMap(), ); @@ -6539,9 +6552,10 @@ class App extends React.Component { } if (this.state.activeTool.type === "lasso") { + const [pointerDownOriginX, pointerDownOriginY] = pointerDownState.origin; this.lassoTrail.startPath( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownOriginX, + pointerDownOriginY, event.shiftKey, ); } else if (this.state.activeTool.type === "text") { @@ -6574,9 +6588,9 @@ class App extends React.Component { multiElement: null, }); - const { x, y } = viewportCoordsToSceneCoords(event, this.state); + const [x, y] = viewportCoordsToSceneCoords(event, this.state); - const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); + const frame = this.getTopLayerFrameAtSceneCoords(pointFrom(x, y)); mutateElement(pendingImageElement, { x, @@ -6600,9 +6614,11 @@ class App extends React.Component { this.state.activeTool.type, ); } else if (this.state.activeTool.type === "laser") { + const [pointerDownLastCoordsX, pointerDownLastCoordsY] = + pointerDownState.lastCoords; this.laserTrails.startPath( - pointerDownState.lastCoords.x, - pointerDownState.lastCoords.y, + pointerDownLastCoordsX, + pointerDownLastCoordsY, ); } else if ( this.state.activeTool.type !== "eraser" && @@ -6622,9 +6638,11 @@ class App extends React.Component { ); if (this.state.activeTool.type === "eraser") { + const [pointerDownLastCoordsX, pointerDownLastCoordsY] = + pointerDownState.lastCoords; this.eraserTrail.startPath( - pointerDownState.lastCoords.x, - pointerDownState.lastCoords.y, + pointerDownLastCoordsX, + pointerDownLastCoordsY, ); } @@ -6663,21 +6681,22 @@ class App extends React.Component { { clientX: event.clientX, clientY: event.clientY }, this.state, ); + const [scenePointerX, scenePointerY] = scenePointer; const clicklength = event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); if (this.device.editor.isMobile && clicklength < 300) { const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ); if ( isIframeLikeElement(hitElement) && this.isIframeLikeElementCenter( hitElement, event, - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ) ) { this.handleEmbeddableCenterClick(hitElement); @@ -6687,8 +6706,8 @@ class App extends React.Component { if (this.device.isTouchScreen) { const hitElement = this.getElementAtPosition( - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ); this.hitLinkElement = this.getElementLinkAtPosition( scenePointer, @@ -6707,7 +6726,7 @@ class App extends React.Component { this.hitLinkElement, this.scene.getNonDeletedElementsMap(), this.state, - pointFrom(scenePointer.x, scenePointer.y), + pointFrom(scenePointerX, scenePointerY), ) ) { this.handleEmbeddableCenterClick(this.hitLinkElement); @@ -6878,35 +6897,38 @@ 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, + pointFrom(event.clientX, event.clientY), + ); if (gesture.pointers.size === 2) { gesture.lastCenter = getCenter(gesture.pointers); gesture.initialScale = this.state.zoom.value; - gesture.initialDistance = getDistance( - Array.from(gesture.pointers.values()), - ); + + const gesturePointers = Array.from(gesture.pointers.values()); + gesture.initialDistance = isTwoPointerCoords(gesturePointers) + ? getDistance(gesturePointers) + : 0; } } private initialPointerDownState( event: React.PointerEvent, ): PointerDownState { - const origin = viewportCoordsToSceneCoords(event, this.state); + const originCoords = viewportCoordsToSceneCoords(event, this.state); + const [originX, originY] = originCoords; const selectedElements = this.scene.getSelectedElements(this.state); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow) === 0; return { - origin, + origin: originCoords, withCmdOrCtrl: event[KEYS.CTRL_OR_CMD], originInGrid: tupleToCoors( getGridPoint( - origin.x, - origin.y, + originX, + originY, event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly ? null : this.getEffectiveGridSize(), @@ -6918,7 +6940,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: originCoords, originalElements: this.scene .getNonDeletedElements() .reduce((acc, element) => { @@ -6928,9 +6950,9 @@ class App extends React.Component { resize: { handleType: false, isResizing: false, - offset: { x: 0, y: 0 }, + offset: pointFrom(0, 0), arrowDirection: "origin", - center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 }, + center: pointFrom((maxX + minX) / 2, (maxY + minY) / 2), }, hit: { element: null, @@ -6939,14 +6961,14 @@ class App extends React.Component { hasBeenDuplicated: false, hasHitCommonBoundingBoxOfSelectedElements: this.isHittingCommonBoundingBoxOfSelectedElements( - origin, + originCoords, selectedElements, ), }, drag: { hasOccurred: false, offset: null, - origin: { ...origin }, + origin: originCoords, }, eventListeners: { onMove: null, @@ -6971,8 +6993,8 @@ class App extends React.Component { return false; } isDraggingScrollBar = true; - pointerDownState.lastCoords.x = event.clientX; - pointerDownState.lastCoords.y = event.clientY; + [pointerDownState.lastCoords[0], pointerDownState.lastCoords[1]] = + pointFrom(event.clientX, event.clientY); const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => { const target = event.target; if (!(target instanceof HTMLElement)) { @@ -7026,6 +7048,7 @@ class App extends React.Component { const elements = this.scene.getNonDeletedElements(); const elementsMap = this.scene.getNonDeletedElementsMap(); const selectedElements = this.scene.getSelectedElements(this.state); + const [originX, originY] = pointerDownState.origin; if ( selectedElements.length === 1 && @@ -7040,8 +7063,8 @@ class App extends React.Component { getElementWithTransformHandleType( elements, this.state, - pointerDownState.origin.x, - pointerDownState.origin.y, + originX, + originY, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap(), @@ -7070,8 +7093,8 @@ class App extends React.Component { } else if (selectedElements.length > 1) { pointerDownState.resize.handleType = getTransformHandleTypeFromCoords( getCommonBounds(selectedElements), - pointerDownState.origin.x, - pointerDownState.origin.y, + originX, + originY, this.state.zoom, event.pointerType, this.device, @@ -7084,8 +7107,8 @@ class App extends React.Component { pointerDownState.resize.handleType, selectedElements, elementsMap, - pointerDownState.origin.x, - pointerDownState.origin.y, + originX, + originY, ), ); if ( @@ -7127,10 +7150,7 @@ 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(originX, originY); this.hitLinkElement = this.getElementLinkAtPosition( pointerDownState.origin, @@ -7151,10 +7171,7 @@ class App extends React.Component { 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) { @@ -7165,8 +7182,8 @@ 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, + originX, + originY, ); const hitElement = pointerDownState.hit.element; @@ -7355,8 +7372,10 @@ class App extends React.Component { return hitElement != null && this.state.selectedElementIds[hitElement.id]; } - private isHittingCommonBoundingBoxOfSelectedElements( - point: Readonly<{ x: number; y: number }>, + private isHittingCommonBoundingBoxOfSelectedElements< + Point extends GenericPoint, + >( + point: Readonly, selectedElements: readonly ExcalidrawElement[], ): boolean { if (selectedElements.length < 2) { @@ -7366,11 +7385,12 @@ class App extends React.Component { // How many pixels off the shape boundary we still consider a hit const threshold = this.getElementHitThreshold(); const [x1, y1, x2, y2] = getCommonBounds(selectedElements); + const [pointX, pointY] = point; return ( - point.x > x1 - threshold && - point.x < x2 + threshold && - point.y > y1 - threshold && - point.y < y2 + threshold + pointX > x1 - threshold && + pointX < x2 + threshold && + pointY > y1 - threshold && + pointY < y2 + threshold ); } @@ -7384,8 +7404,7 @@ class App extends React.Component { if (this.state.editingTextElement) { return; } - let sceneX = pointerDownState.origin.x; - let sceneY = pointerDownState.origin.y; + let [sceneX, sceneY] = pointerDownState.origin; const element = this.getElementAtPosition(sceneX, sceneY, { includeBoundTextElement: true, @@ -7420,17 +7439,12 @@ class App extends React.Component { elementType: ExcalidrawFreeDrawElement["type"], pointerDownState: PointerDownState, ) => { + const [originX, originY] = pointerDownState.origin; // Begin a mark capture. This does not have to update state yet. - const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, - null, - ); + const gridPoint = getGridPoint(originX, originY, null); + const [gridX, gridY] = gridPoint; - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(gridPoint); const simulatePressure = event.pressure === 0.5; @@ -7586,19 +7600,17 @@ class App extends React.Component { sceneY: number; addToFrameUnderCursor?: boolean; }) => { - const [gridX, gridY] = getGridPoint( + const gridPoint = getGridPoint( sceneX, sceneY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [gridX, gridY] = gridPoint; const topLayerFrame = addToFrameUnderCursor - ? this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }) + ? this.getTopLayerFrameAtSceneCoords(gridPoint) : null; const element = newImageElement({ @@ -7652,19 +7664,14 @@ class App extends React.Component { return; } - const { x: rx, y: ry, lastCommittedPoint } = multiElement; + const { lastCommittedPoint } = multiElement; // clicking inside commit zone → finalize arrow if ( multiElement.points.length > 1 && lastCommittedPoint && - pointDistance( - pointFrom( - pointerDownState.origin.x - rx, - pointerDownState.origin.y - ry, - ), - lastCommittedPoint, - ) < LINE_CONFIRM_THRESHOLD + pointDistance(pointerDownState.origin, lastCommittedPoint) < + LINE_CONFIRM_THRESHOLD ) { this.actionManager.executeAction(actionFinalize); return; @@ -7686,16 +7693,15 @@ class App extends React.Component { }); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } else { - const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + const [originX, originY] = pointerDownState.origin; + const gridPoint = getGridPoint( + originX, + originY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [gridX, gridY] = gridPoint; - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(gridPoint); /* 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 @@ -7810,18 +7816,17 @@ class App extends React.Component { elementType: ExcalidrawGenericElement["type"] | "embeddable", pointerDownState: PointerDownState, ): void => { - const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + const [originX, originY] = pointerDownState.origin; + const gridPoint = getGridPoint( + originX, + originY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [gridX, gridY] = gridPoint; - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords(gridPoint); const baseElementAttributes = { x: gridX, @@ -7868,9 +7873,10 @@ class App extends React.Component { pointerDownState: PointerDownState, type: Extract, ): void => { + const [originX, originY] = pointerDownState.origin; const [gridX, gridY] = getGridPoint( - pointerDownState.origin.x, - pointerDownState.origin.y, + originX, + originY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), @@ -7977,17 +7983,19 @@ class App extends React.Component { return; } const pointerCoords = viewportCoordsToSceneCoords(event, this.state); + const [pointerCoordsX, pointerCoordsY] = pointerCoords; if ( this.state.selectedLinearElement && this.state.selectedLinearElement.elbowed && this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index ) { - const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + const gridPoint = getGridPoint( + pointerCoordsX, + pointerCoordsY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [gridX, gridY] = gridPoint; let index = this.state.selectedLinearElement.pointerDownState.segmentMidpoint @@ -7998,7 +8006,7 @@ class App extends React.Component { ...this.state.selectedLinearElement, segmentMidPointHoveredCoords: null, }, - { x: gridX, y: gridY }, + gridPoint, this.state, this.scene.getNonDeletedElementsMap(), ); @@ -8034,9 +8042,9 @@ class App extends React.Component { return; } - const lastPointerCoords = - this.lastPointerMoveCoords ?? pointerDownState.origin; - this.lastPointerMoveCoords = pointerCoords; + // const lastPointerCoords = + // this.lastPointerMoveCoords ?? pointerDownState.origin; + // this.lastPointerMoveCoords = pointerCoords; // We need to initialize dragOffsetXY only after we've updated // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove @@ -8046,8 +8054,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], ), ); } @@ -8066,12 +8074,12 @@ class App extends React.Component { } if (this.state.activeTool.type === "laser") { - this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); + this.laserTrails.addPointToPath(pointerCoordsX, pointerCoordsY); } const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -8085,17 +8093,18 @@ class App extends React.Component { this.state.activeTool.type === "line") ) { if ( - pointDistance( - pointFrom(pointerCoords.x, pointerCoords.y), - pointFrom(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; + const updatedPointerDownState: PointerDownState = { + ...pointerDownState, + lastCoords: pointFrom(pointerCoordsX, pointerCoordsY), + }; + pointerDownState = updatedPointerDownState; if (this.maybeHandleCrop(pointerDownState, event)) { return true; } @@ -8166,8 +8175,8 @@ class App extends React.Component { const newLinearElementEditor = LinearElementEditor.handlePointDragging( event, this, - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, (element, pointsSceneCoords) => { this.maybeSuggestBindingsForLinearElementAtCoords( element, @@ -8178,9 +8187,15 @@ class App extends React.Component { this.scene, ); if (newLinearElementEditor) { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; - pointerDownState.drag.hasOccurred = true; + const updatedPointerDownState: PointerDownState = { + ...pointerDownState, + lastCoords: pointFrom(pointerCoordsX, pointerCoordsY), + drag: { + ...pointerDownState.drag, + hasOccurred: true, + }, + }; + pointerDownState = updatedPointerDownState; this.setState({ editingLinearElement: this.state.editingLinearElement @@ -8240,9 +8255,11 @@ class App extends React.Component { !this.state.editingTextElement && this.state.activeEmbeddable?.state !== "active" ) { + const [pointerDownOriginX, pointerDownOriginY] = + pointerDownState.origin; const dragOffset = { - x: pointerCoords.x - pointerDownState.drag.origin.x, - y: pointerCoords.y - pointerDownState.drag.origin.y, + x: pointerCoordsX - pointerDownOriginX, + y: pointerCoordsY - pointerDownOriginY, }; const originalElements = [ @@ -8286,10 +8303,11 @@ class App extends React.Component { this.imageCache.get(croppingElement.fileId)?.image; if (image && !(image instanceof Promise)) { + const [lastCoordsX, lastCoordsY] = pointerDownState.origin; const instantDragOffset = vectorScale( vector( - pointerCoords.x - lastPointerCoords.x, - pointerCoords.y - lastPointerCoords.y, + pointerCoordsX - lastCoordsX, + pointerCoordsY - lastCoordsY, ), Math.max(this.state.zoom.value, 2), ); @@ -8550,16 +8568,21 @@ class App extends React.Component { } if (this.state.selectionElement) { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + const updatedPointerDownState: PointerDownState = { + ...pointerDownState, + lastCoords: pointFrom(pointerCoordsX, pointerCoordsY), + }; + pointerDownState = updatedPointerDownState; if (event.altKey) { this.setActiveTool( { type: "lasso", fromSelection: true }, event.shiftKey, ); + const [pointerDownOriginX, pointerDownOriginY] = + pointerDownState.origin; this.lassoTrail.startPath( - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownOriginX, + pointerDownOriginY, event.shiftKey, ); this.setAppState({ @@ -8572,14 +8595,18 @@ class App extends React.Component { if (!event.altKey && this.state.activeTool.fromSelection) { this.setActiveTool({ type: "selection" }); this.createGenericElementOnPointerDown("selection", pointerDownState); - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + + const updatedPointerDownState: PointerDownState = { + ...pointerDownState, + lastCoords: pointFrom(pointerCoordsX, pointerCoordsY), + }; + pointerDownState = updatedPointerDownState; this.maybeDragNewGenericElement(pointerDownState, event); this.lassoTrail.endPath(); } else { this.lassoTrail.addPointToPath( - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, event.shiftKey, ); } @@ -8594,8 +8621,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 = pointerCoordsX - newElement.x; + const dy = pointerCoordsY - newElement.y; const lastPoint = points.length > 0 && points[points.length - 1]; const discardPoint = @@ -8629,8 +8656,8 @@ class App extends React.Component { ({ width: dx, height: dy } = getLockedLinearCursorAlignSize( newElement.x, newElement.y, - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, )); } @@ -8669,8 +8696,11 @@ class App extends React.Component { ); } } else { - pointerDownState.lastCoords.x = pointerCoords.x; - pointerDownState.lastCoords.y = pointerCoords.y; + const updatedPointerDownState: PointerDownState = { + ...pointerDownState, + lastCoords: pointFrom(pointerCoordsX, pointerCoordsY), + }; + pointerDownState = updatedPointerDownState; this.maybeDragNewGenericElement(pointerDownState, event, false); } } @@ -8783,29 +8813,32 @@ class App extends React.Component { event: PointerEvent, pointerDownState: PointerDownState, ): boolean { + const [pointerLastCoordsX, pointerLastCoordsY] = + pointerDownState.lastCoords; + if (pointerDownState.scrollbars.isOverHorizontal) { const x = event.clientX; - const dx = x - pointerDownState.lastCoords.x; + const dx = x - pointerLastCoordsX; this.translateCanvas({ scrollX: this.state.scrollX - (dx * (currentScrollBars.horizontal?.deltaMultiplier || 1)) / this.state.zoom.value, }); - pointerDownState.lastCoords.x = x; + pointerDownState.lastCoords[0] = x; return true; } if (pointerDownState.scrollbars.isOverVertical) { const y = event.clientY; - const dy = y - pointerDownState.lastCoords.y; + const dy = y - pointerLastCoordsY; this.translateCanvas({ scrollY: this.state.scrollY - (dy * (currentScrollBars.vertical?.deltaMultiplier || 1)) / this.state.zoom.value, }); - pointerDownState.lastCoords.y = y; + pointerDownState.lastCoords[1] = y; return true; } return false; @@ -8982,14 +9015,14 @@ class App extends React.Component { ); if (newElement?.type === "freedraw") { - const pointerCoords = viewportCoordsToSceneCoords( + const [pointerCoordsX, pointerCoordsY] = viewportCoordsToSceneCoords( childEvent, this.state, ); const points = newElement.points; - let dx = pointerCoords.x - newElement.x; - let dy = pointerCoords.y - newElement.y; + let dx = pointerCoordsX - newElement.x; + let dy = pointerCoordsY - newElement.y; // Allows dots to avoid being flagged as infinitely small if (dx === points[0][0] && dy === points[0][1]) { @@ -9046,14 +9079,15 @@ class App extends React.Component { childEvent, this.state, ); + const [pointerCoordsX, pointerCoordsY] = pointerCoords; if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { mutateElement(newElement, { points: [ ...newElement.points, pointFrom( - pointerCoords.x - newElement.x, - pointerCoords.y - newElement.y, + pointerCoordsX - newElement.x, + pointerCoordsY - newElement.y, ), ], }); @@ -9386,7 +9420,7 @@ class App extends React.Component { ); if (draggedDistance === 0) { - const scenePointer = viewportCoordsToSceneCoords( + const [scenePointerX, scenePointerY] = viewportCoordsToSceneCoords( { clientX: pointerEnd.clientX, clientY: pointerEnd.clientY, @@ -9394,8 +9428,8 @@ class App extends React.Component { this.state, ); const hitElements = this.getElementsAtPosition( - scenePointer.x, - scenePointer.y, + scenePointerX, + scenePointerY, ); hitElements.forEach((hitElement) => this.elementsPendingErasure.add(hitElement.id), @@ -9560,6 +9594,8 @@ class App extends React.Component { } } + const [pointerDownOriginX, pointerDownOriginY] = pointerDownState.origin; + if ( // do not clear selection if lasso is active this.state.activeTool.type !== "lasso" && @@ -9573,8 +9609,8 @@ class App extends React.Component { ((hitElement && hitElementBoundingBoxOnly( { - x: pointerDownState.origin.x, - y: pointerDownState.origin.y, + x: pointerDownOriginX, + y: pointerDownOriginY, element: hitElement, shape: getElementShape( hitElement, @@ -9693,8 +9729,8 @@ class App extends React.Component { this.isIframeLikeElementCenter( hitElement, this.lastPointerUpEvent, - pointerDownState.origin.x, - pointerDownState.origin.y, + pointerDownOriginX, + pointerDownOriginY, ) ) { this.handleEmbeddableCenterClick(hitElement); @@ -9946,7 +9982,7 @@ 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( + const [x, y] = viewportCoordsToSceneCoords( { clientX, clientY }, this.state, ); @@ -10143,11 +10179,8 @@ class App extends React.Component { } }; - private maybeSuggestBindingAtCursor = ( - pointerCoords: { - x: number; - y: number; - }, + private maybeSuggestBindingAtCursor = ( + pointerCoords: Point, considerAll: boolean, ): void => { const hoveredBindableElement = getHoveredElementForBinding( @@ -10164,13 +10197,12 @@ class App extends React.Component { }); }; - private maybeSuggestBindingsForLinearElementAtCoords = ( + private maybeSuggestBindingsForLinearElementAtCoords = < + Point extends GenericPoint, + >( linearElement: NonDeleted, /** scene coords */ - pointerCoords: { - x: number; - y: number; - }[], + pointerCoords: Point[], // During line creation the start binding hasn't been written yet // into `linearElement` oppositeBindingBoundElement?: ExcalidrawBindableElement | null, @@ -10259,10 +10291,7 @@ 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, - this.state, - ); + const [sceneX, sceneY] = viewportCoordsToSceneCoords(event, this.state); try { // if image tool not supported, don't show an error here and let it fall @@ -10452,7 +10481,8 @@ class App extends React.Component { return; } - const { x, y } = viewportCoordsToSceneCoords(event, this.state); + const viewportCoords = viewportCoordsToSceneCoords(event, this.state); + const [x, y] = viewportCoords; const element = this.getElementAtPosition(x, y, { preferSelected: true, includeLockedElements: true, @@ -10461,7 +10491,7 @@ class App extends React.Component { const selectedElements = this.scene.getSelectedElements(this.state); const isHittingCommonBoundBox = this.isHittingCommonBoundingBoxOfSelectedElements( - { x, y }, + viewportCoords, selectedElements, ); @@ -10510,17 +10540,24 @@ class App extends React.Component { informMutation = true, ): void => { const selectionElement = this.state.selectionElement; - const pointerCoords = pointerDownState.lastCoords; + const [pointerCoordsX, pointerCoordsY] = pointerDownState.lastCoords; + const [pointerDownOriginX, pointerDownOriginY] = pointerDownState.origin; + const [pointerDownOriginInGridX, pointerDownOriginInGridY] = + pointerDownState.originInGrid; + + const [stateOriginSnapOffsetX, stateOriginSnapOffsetY] = + this.state.originSnapOffset ?? pointFrom(0, 0); + if (selectionElement && this.state.activeTool.type !== "eraser") { 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: pointerDownOriginX, + originY: pointerDownOriginY, + x: pointerCoordsX, + y: pointerCoordsY, + width: distance(pointerDownOriginX, pointerCoordsX), + height: distance(pointerDownOriginY, pointerCoordsY), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), shouldResizeFromCenter: false, zoom: this.state.zoom.value, @@ -10535,8 +10572,8 @@ class App extends React.Component { } let [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -10553,16 +10590,12 @@ class App extends React.Component { this, event, { - x: - pointerDownState.originInGrid.x + - (this.state.originSnapOffset?.x ?? 0), - y: - pointerDownState.originInGrid.y + - (this.state.originSnapOffset?.y ?? 0), + x: pointerDownOriginInGridX + stateOriginSnapOffsetX, + y: pointerDownOriginInGridY + stateOriginSnapOffsetY, }, { - x: gridX - pointerDownState.originInGrid.x, - y: gridY - pointerDownState.originInGrid.y, + x: gridX - pointerDownOriginInGridX, + y: gridY - pointerDownOriginInGridY, }, this.scene.getNonDeletedElementsMap(), ); @@ -10577,12 +10610,12 @@ class App extends React.Component { dragNewElement({ newElement, elementType: this.state.activeTool.type, - originX: pointerDownState.originInGrid.x, - originY: pointerDownState.originInGrid.y, + originX: pointerDownOriginInGridX, + originY: pointerDownOriginInGridY, x: gridX, y: gridY, - width: distance(pointerDownState.originInGrid.x, gridX), - height: distance(pointerDownState.originInGrid.y, gridY), + width: distance(pointerDownOriginInGridX, gridX), + height: distance(pointerDownOriginInGridY, gridY), shouldMaintainAspectRatio: isImageElement(newElement) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event), @@ -10623,10 +10656,11 @@ class App extends React.Component { } const transformHandleType = pointerDownState.resize.handleType; - const pointerCoords = pointerDownState.lastCoords; + const [pointerCoordsX, pointerCoordsY] = pointerDownState.lastCoords; + const [resizeOffsetX, resizeOffsetY] = pointerDownState.resize.offset; const [x, y] = getGridPoint( - pointerCoords.x - pointerDownState.resize.offset.x, - pointerCoords.y - pointerDownState.resize.offset.y, + pointerCoordsX - resizeOffsetX, + pointerCoordsY - resizeOffsetY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); @@ -10654,14 +10688,16 @@ class App extends React.Component { !(image instanceof Promise) ) { const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [pointerDownOriginInGridX, pointerDownOriginInGridY] = + pointerDownState.originInGrid; const dragOffset = { - x: gridX - pointerDownState.originInGrid.x, - y: gridY - pointerDownState.originInGrid.y, + x: gridX - pointerDownOriginInGridX, + y: gridY - pointerDownOriginInGridY, }; this.maybeCacheReferenceSnapPoints(event, [croppingElement]); @@ -10744,20 +10780,16 @@ class App extends React.Component { isRotating: transformHandleType === "rotation", activeEmbeddable: null, }); - const pointerCoords = pointerDownState.lastCoords; + + const [pointerCoordsX, pointerCoordsY] = pointerDownState.lastCoords; + const [resizeOffsetX, resizeOffsetY] = pointerDownState.resize.offset; let [resizeX, resizeY] = getGridPoint( - pointerCoords.x - pointerDownState.resize.offset.x, - pointerCoords.y - pointerDownState.resize.offset.y, + pointerCoordsX - resizeOffsetX, + pointerCoordsY - resizeOffsetY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); - const frameElementsOffsetsMap = new Map< - string, - { - x: number; - y: number; - } - >(); + const frameElementsOffsetsMap = new Map(); selectedFrames.forEach((frame) => { const elementsInFrame = getFrameChildren( @@ -10766,10 +10798,10 @@ class App extends React.Component { ); elementsInFrame.forEach((element) => { - frameElementsOffsetsMap.set(frame.id + element.id, { - x: element.x - frame.x, - y: element.y - frame.y, - }); + frameElementsOffsetsMap.set( + frame.id + element.id, + pointFrom(element.x - frame.x, element.y - frame.y), + ); }); }); @@ -10777,14 +10809,16 @@ class App extends React.Component { // during dragging if (!this.state.selectedElementsAreBeingDragged) { const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, + pointerCoordsX, + pointerCoordsY, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); + const [pointerDownOriginInGridX, pointerDownOriginInGridY] = + pointerDownState.originInGrid; const dragOffset = { - x: gridX - pointerDownState.originInGrid.x, - y: gridY - pointerDownState.originInGrid.y, + x: gridX - pointerDownOriginInGridX, + y: gridY - pointerDownOriginInGridY, }; const originalElements = [...pointerDownState.originalElements.values()]; @@ -10808,6 +10842,9 @@ class App extends React.Component { }); } + const [pointerDownResizeCenterX, pointerDownResizeCenterY] = + pointerDownState.resize.center; + if ( transformElements( pointerDownState.originalElements, @@ -10822,8 +10859,8 @@ class App extends React.Component { : shouldMaintainAspectRatio(event), resizeX, resizeY, - pointerDownState.resize.center.x, - pointerDownState.resize.center.y, + pointerDownResizeCenterX, + pointerDownResizeCenterY, ) ) { const suggestedBindings = getSuggestedBindingsForArrows( @@ -10991,11 +11028,14 @@ class App extends React.Component { // reduced amplification for small deltas (small movements on a trackpad) Math.min(1, absDelta / 20); + const [lastViewportPositionX, lastViewportPositionY] = + this.lastViewportPosition; + this.translateCanvas((state) => ({ ...getStateForZoom( { - viewportX: this.lastViewportPosition.x, - viewportY: this.lastViewportPosition.y, + viewportX: lastViewportPositionX, + viewportY: lastViewportPositionY, nextZoom: getNormalizedZoom(newZoom), }, state, @@ -11048,7 +11088,7 @@ class App extends React.Component { const isSnappedToCenter = distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD; if (isSnappedToCenter) { - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( + const [viewportX, viewportY] = sceneCoordsToViewportCoords( { sceneX: elementCenterX, sceneY: elementCenterY }, appState, ); @@ -11061,7 +11101,7 @@ class App extends React.Component { if (!x || !y) { return; } - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( + const [sceneX, sceneY] = viewportCoordsToSceneCoords( { clientX: x, clientY: y }, this.state, ); diff --git a/packages/excalidraw/components/ElementCanvasButtons.tsx b/packages/excalidraw/components/ElementCanvasButtons.tsx index 424c4f3b4..ddfdab961 100644 --- a/packages/excalidraw/components/ElementCanvasButtons.tsx +++ b/packages/excalidraw/components/ElementCanvasButtons.tsx @@ -20,7 +20,7 @@ const getContainerCoords = ( elementsMap: ElementsMap, ) => { const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( + const [viewportX, viewportY] = sceneCoordsToViewportCoords( { sceneX: x1 + element.width, sceneY: y1 }, appState, ); diff --git a/packages/excalidraw/components/EyeDropper.tsx b/packages/excalidraw/components/EyeDropper.tsx index f7f98123d..2d02aa96a 100644 --- a/packages/excalidraw/components/EyeDropper.tsx +++ b/packages/excalidraw/components/EyeDropper.tsx @@ -166,9 +166,14 @@ export const EyeDropper: React.FC<{ eyeDropperContainer.focus(); // init color preview else it would show only after the first mouse move + const [ + stablePropsAppLastViewportPositionX, + stablePropsAppLastViewportPositionY, + ] = stableProps.app.lastViewportPosition; + mouseMoveListener({ - clientX: stableProps.app.lastViewportPosition.x, - clientY: stableProps.app.lastViewportPosition.y, + clientX: stablePropsAppLastViewportPositionX, + clientY: stablePropsAppLastViewportPositionY, altKey: false, }); diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx index 9a386a163..ce36ce5bc 100644 --- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx +++ b/packages/excalidraw/components/hyperlink/Hyperlink.tsx @@ -360,7 +360,7 @@ const getCoordsForPopover = ( elementsMap: ElementsMap, ) => { const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); - const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( + const [viewportX, viewportY] = sceneCoordsToViewportCoords( { sceneX: x1 + element.width / 2, sceneY: y1 }, appState, ); @@ -422,7 +422,7 @@ const renderTooltip = ( appState, ); - const linkViewportCoords = sceneCoordsToViewportCoords( + const [linkViewportCoordX, linkViewportCoordY] = sceneCoordsToViewportCoords( { sceneX: linkX, sceneY: linkY }, appState, ); @@ -430,8 +430,8 @@ const renderTooltip = ( updateTooltipPosition( tooltipDiv, { - left: linkViewportCoords.x, - top: linkViewportCoords.y, + left: linkViewportCoordX, + top: linkViewportCoordY, width: linkWidth, height: linkHeight, }, @@ -457,7 +457,7 @@ const shouldHideLinkPopup = ( appState: AppState, [clientX, clientY]: GlobalPoint, ): Boolean => { - const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( + const [sceneX, sceneY] = viewportCoordsToSceneCoords( { clientX, clientY }, appState, ); diff --git a/packages/excalidraw/gesture.ts b/packages/excalidraw/gesture.ts index 8ffa6d2bb..f043de3bd 100644 --- a/packages/excalidraw/gesture.ts +++ b/packages/excalidraw/gesture.ts @@ -1,15 +1,25 @@ -import type { PointerCoords } from "./types"; +import { pointFrom, type GenericPoint } from "@excalidraw/math"; -export const getCenter = (pointers: Map) => { +export const getCenter = ( + pointers: Map, +): Point => { const allCoords = Array.from(pointers.values()); - return { - x: sum(allCoords, (coords) => coords.x) / allCoords.length, - y: sum(allCoords, (coords) => coords.y) / allCoords.length, - }; + return pointFrom( + sum(allCoords, ([coordsX, _]) => coordsX) / allCoords.length, + sum(allCoords, ([_, coordsY]) => coordsY) / allCoords.length, + ); }; -export const getDistance = ([a, b]: readonly PointerCoords[]) => - Math.hypot(a.x - b.x, a.y - b.y); +export const isTwoPointerCoords = ( + arr: Point[], +): arr is [Point, Point] => { + return arr.length === 2; +}; + +export const getDistance = ([ + [x1, y1], + [x2, y2], +]: readonly [Point, Point]) => Math.hypot(x1 - x2, y1 - y2); const sum = (array: readonly T[], mapper: (item: T) => number): number => array.reduce((acc, item) => acc + mapper(item), 0); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 69c6a8196..da85d1c4c 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -2,8 +2,8 @@ import oc from "open-color"; import { pointFrom, type GlobalPoint, - type LocalPoint, type Radians, + type GenericPoint, } from "@excalidraw/math"; import { @@ -144,7 +144,7 @@ const renderLinearElementPointHighlight = ( context.restore(); }; -const highlightPoint = ( +const highlightPoint = ( point: Point, context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, @@ -211,7 +211,7 @@ const strokeDiamondWithRotation = ( context.restore(); }; -const renderSingleLinearPoint = ( +const renderSingleLinearPoint = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, point: Point, diff --git a/packages/excalidraw/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts index dd131f779..ade8a6b83 100644 --- a/packages/excalidraw/renderer/renderSnaps.ts +++ b/packages/excalidraw/renderer/renderSnaps.ts @@ -1,4 +1,4 @@ -import { pointFrom, type GlobalPoint, type LocalPoint } from "@excalidraw/math"; +import { pointFrom, type GenericPoint } from "@excalidraw/math"; import { THEME } from "@excalidraw/common"; @@ -88,7 +88,7 @@ const drawPointerSnapLine = ( } }; -const drawCross = ( +const drawCross = ( [x, y]: Point, appState: InteractiveCanvasAppState, context: CanvasRenderingContext2D, @@ -109,7 +109,7 @@ const drawCross = ( context.restore(); }; -const drawLine = ( +const drawLine = ( from: Point, to: Point, context: CanvasRenderingContext2D, @@ -120,7 +120,7 @@ const drawLine = ( context.stroke(); }; -const drawGapLine = ( +const drawGapLine = ( from: Point, to: Point, direction: "horizontal" | "vertical", diff --git a/packages/excalidraw/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts index a99ad075f..8c3be05bf 100644 --- a/packages/excalidraw/scene/scroll.ts +++ b/packages/excalidraw/scene/scroll.ts @@ -3,6 +3,7 @@ import { sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, } from "@excalidraw/common"; +import { pointFrom } from "@excalidraw/math"; import { getClosestElementBounds } from "@excalidraw/element/bounds"; @@ -14,11 +15,11 @@ import type { AppState, Offsets, PointerCoords, Zoom } from "../types"; const isOutsideViewPort = (appState: AppState, cords: Array) => { const [x1, y1, x2, y2] = cords; - const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords( + const [viewportX1, viewportY1] = sceneCoordsToViewportCoords( { sceneX: x1, sceneY: y1 }, appState, ); - const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords( + const [viewportX2, viewportY2] = sceneCoordsToViewportCoords( { sceneX: x2, sceneY: y2 }, appState, ); @@ -39,15 +40,16 @@ export const centerScrollOn = ({ zoom: Zoom; offsets?: Offsets; }) => { + const [scenePointX, scenePointY] = scenePoint; let scrollX = (viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value - - scenePoint.x; + scenePointX; scrollX += (offsets?.left ?? 0) / 2 / zoom.value; let scrollY = (viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value - - scenePoint.y; + scenePointY; scrollY += (offsets?.top ?? 0) / 2 / zoom.value; @@ -85,7 +87,7 @@ export const calculateScrollCenter = ( const centerY = (y1 + y2) / 2; return centerScrollOn({ - scenePoint: { x: centerX, y: centerY }, + scenePoint: pointFrom(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 12a5e27a8..2c2f3f3b0 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -7,6 +7,7 @@ import type { } from "@excalidraw/element/types"; import type { MakeBrand } from "@excalidraw/common/utility-types"; +import type { GenericPoint } from "@excalidraw/math"; import type { AppClassProperties, @@ -61,7 +62,7 @@ export type InteractiveCanvasRenderConfig = { // collab-related state // --------------------------------------------------------------------------- remoteSelectedElementIds: Map; - remotePointerViewportCoords: Map; + remotePointerViewportCoords: Map; remotePointerUserStates: Map; remotePointerUsernames: Map; remotePointerButton: Map; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 6ea23bd87..3204fee80 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -1327,7 +1327,7 @@ export const getSnapLinesAtPointer = ( ) => { if (!isSnappingEnabled({ event, selectedElements: [], app })) { return { - originOffset: { x: 0, y: 0 }, + originOffset: pointFrom(0, 0), snapLines: [], }; } @@ -1388,16 +1388,14 @@ export const getSnapLinesAtPointer = ( } return { - originOffset: { - x: - verticalSnapLines.length > 0 - ? verticalSnapLines[0].points[0][0] - pointer.x - : 0, - y: - horizontalSnapLines.length > 0 - ? horizontalSnapLines[0].points[0][1] - pointer.y - : 0, - }, + originOffset: pointFrom( + verticalSnapLines.length > 0 + ? verticalSnapLines[0].points[0][0] - pointer.x + : 0, + horizontalSnapLines.length > 0 + ? horizontalSnapLines[0].points[0][1] - pointer.y + : 0, + ), snapLines: [...verticalSnapLines, ...horizontalSnapLines], }; }; diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 349dd9e64..3807caa94 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -945,10 +945,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -7758,10 +7758,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -9754,10 +9754,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 7b249da27..1cdcedf7d 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -1178,10 +1178,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -1550,10 +1550,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -1923,10 +1923,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -2634,10 +2634,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -2937,10 +2937,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -3225,10 +3225,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -3523,10 +3523,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -3813,10 +3813,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -4052,10 +4052,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -4315,10 +4315,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -4592,10 +4592,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -4827,10 +4827,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -5062,10 +5062,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -5295,10 +5295,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -5528,10 +5528,10 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -6937,10 +6937,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -9984,10 +9984,10 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -10872,10 +10872,10 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -12476,10 +12476,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -13804,10 +13804,10 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -14272,10 +14272,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -14822,10 +14822,10 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 68d4d5d79..3908eedcc 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -8566,10 +8566,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "value": null, }, }, - "pointerOffset": { - "x": 0, - "y": 0, - }, + "pointerOffset": [ + 0, + 0, + ], "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", @@ -8790,10 +8790,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "value": null, }, }, - "pointerOffset": { - "x": 0, - "y": 0, - }, + "pointerOffset": [ + 0, + 0, + ], "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", @@ -9208,10 +9208,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "value": null, }, }, - "pointerOffset": { - "x": 0, - "y": 0, - }, + "pointerOffset": [ + 0, + 0, + ], "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", @@ -9613,10 +9613,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "value": null, }, }, - "pointerOffset": { - "x": 0, - "y": 0, - }, + "pointerOffset": [ + 0, + 0, + ], "segmentMidPointHoveredCoords": null, "selectedPointsIndices": null, "startBindingElement": "keep", @@ -12796,10 +12796,10 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, @@ -14750,10 +14750,10 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index cba9fbea7..921747b83 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -42,6 +42,7 @@ import type { ValueOf, MakeBrand, } from "@excalidraw/common/utility-types"; +import type { GenericPoint } from "@excalidraw/math"; import type { Action } from "./actions/types"; import type { Spreadsheet } from "./charts"; @@ -413,10 +414,7 @@ export interface AppState { showHyperlinkPopup: false | "info" | "editor"; selectedLinearElement: LinearElementEditor | null; snapLines: readonly SnapLine[]; - originSnapOffset: { - x: number; - y: number; - } | null; + originSnapOffset: GenericPoint | null; objectsSnapModeEnabled: boolean; /** the user's socket id & username who is being followed on the canvas */ userToFollow: UserToFollow | null; @@ -456,14 +454,11 @@ export type Zoom = Readonly<{ value: NormalizedZoomValue; }>; -export type PointerCoords = Readonly<{ - x: number; - y: number; -}>; +export type PointerCoords = Readonly; export type Gesture = { pointers: Map; - lastCenter: { x: number; y: number } | null; + lastCenter: PointerCoords | null; initialDistance: number | null; initialScale: number | null; }; @@ -718,13 +713,13 @@ export type AppClassProperties = { export type PointerDownState = Readonly<{ // 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 }>; + originInGrid: Readonly; // Scrollbar checks scrollbars: ReturnType; // The previous pointer position - lastCoords: { x: number; y: number }; + lastCoords: GenericPoint; // original element frozen snapshots so we can access the original // element attribute values at time of pointerdown originalElements: Map>; @@ -734,11 +729,11 @@ export type PointerDownState = Readonly<{ // This is determined on the initial pointer down event isResizing: boolean; // This is determined on the initial pointer down event - offset: { x: number; y: number }; + offset: GenericPoint; // This is determined on the initial pointer down event arrowDirection: "origin" | "end"; // This is a center point of selected elements determined on the initial pointer down event (for rotation only) - center: { x: number; y: number }; + center: GenericPoint; }; hit: { // The element the pointer is "hitting", is determined on the initial @@ -759,10 +754,10 @@ export type PointerDownState = Readonly<{ // Might change during the pointer interaction hasOccurred: boolean; // Might change during the pointer interaction - offset: { x: number; y: number } | null; + offset: GenericPoint | null; // by default same as PointerDownState.origin. On alt-duplication, reset // to current pointer position at time of duplication. - origin: { x: number; y: number }; + origin: GenericPoint; }; // We need to have these in the state so that we can unsubscribe them eventListeners: { diff --git a/packages/math/src/angle.ts b/packages/math/src/angle.ts index 353dc5dad..8dab50cac 100644 --- a/packages/math/src/angle.ts +++ b/packages/math/src/angle.ts @@ -1,12 +1,6 @@ import { PRECISION } from "./utils"; -import type { - Degrees, - GlobalPoint, - LocalPoint, - PolarCoords, - Radians, -} from "./types"; +import type { Degrees, GenericPoint, PolarCoords, Radians } from "./types"; // TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI export const normalizeRadians = (angle: Radians): Radians => { @@ -24,7 +18,7 @@ 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 =

([ +export const cartesian2Polar =

([ x, y, ]: P): PolarCoords => [ diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index a79fb43a1..06cd31d8e 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -3,7 +3,7 @@ import type { Bounds } from "@excalidraw/element/bounds"; import { isPoint, pointDistance, pointFrom } from "./point"; import { rectangle, rectangleIntersectLineSegment } from "./rectangle"; -import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; +import type { Curve, GenericPoint, LineSegment } from "./types"; /** * @@ -13,7 +13,7 @@ import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; * @param d * @returns */ -export function curve( +export function curve( a: Point, b: Point, c: Point, @@ -82,7 +82,7 @@ function solve( return [t0, s0]; } -const bezierEquation = ( +const bezierEquation = ( c: Curve, t: number, ) => @@ -100,9 +100,10 @@ const bezierEquation = ( /** * Computes the intersection between a cubic spline and a line segment. */ -export function curveIntersectLineSegment< - Point extends GlobalPoint | LocalPoint, ->(c: Curve, l: LineSegment): Point[] { +export function curveIntersectLineSegment( + c: Curve, + l: LineSegment, +): Point[] { // Optimize by doing a cheap bounding box check first const bounds = curveBounds(c); if ( @@ -188,7 +189,7 @@ export function curveIntersectLineSegment< * @param maxLevel * @returns */ -export function curveClosestPoint( +export function curveClosestPoint( c: Curve, p: Point, tolerance: number = 1e-3, @@ -245,7 +246,7 @@ export function curveClosestPoint( * @param c The curve to test * @param p The point to measure from */ -export function curvePointDistance( +export function curvePointDistance( c: Curve, p: Point, ) { @@ -261,9 +262,7 @@ export function curvePointDistance( /** * Determines if the parameter is a Curve */ -export function isCurve

( - v: unknown, -): v is Curve

{ +export function isCurve

(v: unknown): v is Curve

{ return ( Array.isArray(v) && v.length === 4 && @@ -274,9 +273,7 @@ export function isCurve

( ); } -function curveBounds( - c: Curve, -): Bounds { +function curveBounds(c: Curve): Bounds { const [P0, P1, P2, P3] = c; const x = [P0[0], P1[0], P2[0], P3[0]]; const y = [P0[1], P1[1], P2[1], P3[1]]; diff --git a/packages/math/src/ellipse.ts b/packages/math/src/ellipse.ts index 741a77df3..82af3a623 100644 --- a/packages/math/src/ellipse.ts +++ b/packages/math/src/ellipse.ts @@ -13,13 +13,7 @@ import { vectorScale, } from "./vector"; -import type { - Ellipse, - GlobalPoint, - Line, - LineSegment, - LocalPoint, -} from "./types"; +import type { Ellipse, GenericPoint, Line, LineSegment } from "./types"; /** * Construct an Ellipse object from the parameters @@ -30,7 +24,7 @@ import type { * @param halfHeight Half of the height of a non-slanted version of the ellipse * @returns The constructed Ellipse object */ -export function ellipse( +export function ellipse( center: Point, halfWidth: number, halfHeight: number, @@ -49,7 +43,7 @@ export function ellipse( * @param ellipse The ellipse to compare against * @returns TRUE if the point is inside or on the outline of the ellipse */ -export const ellipseIncludesPoint = ( +export const ellipseIncludesPoint = ( p: Point, ellipse: Ellipse, ) => { @@ -69,7 +63,7 @@ export const ellipseIncludesPoint = ( * @param threshold The distance to consider a point close enough to be "on" the outline * @returns TRUE if the point is on the ellise outline */ -export const ellipseTouchesPoint = ( +export const ellipseTouchesPoint = ( point: Point, ellipse: Ellipse, threshold = PRECISION, @@ -85,9 +79,7 @@ export const ellipseTouchesPoint = ( * @param ellipse The ellipse to calculate the distance to * @returns The eucledian distance */ -export const ellipseDistanceFromPoint = < - Point extends GlobalPoint | LocalPoint, ->( +export const ellipseDistanceFromPoint = ( p: Point, ellipse: Ellipse, ): number => { @@ -140,9 +132,10 @@ export const ellipseDistanceFromPoint = < * Calculate a maximum of two intercept points for a line going throug an * ellipse. */ -export function ellipseSegmentInterceptPoints< - Point extends GlobalPoint | LocalPoint, ->(e: Readonly>, s: Readonly>): Point[] { +export function ellipseSegmentInterceptPoints( + e: Readonly>, + s: Readonly>, +): Point[] { const rx = e.halfWidth; const ry = e.halfHeight; @@ -194,9 +187,7 @@ export function ellipseSegmentInterceptPoints< return intersections; } -export function ellipseLineIntersectionPoints< - Point extends GlobalPoint | LocalPoint, ->( +export function ellipseLineIntersectionPoints( { center, halfWidth, halfHeight }: Ellipse, [g, h]: Line, ): Point[] { diff --git a/packages/math/src/line.ts b/packages/math/src/line.ts index 889fa08ce..af3cd58c9 100644 --- a/packages/math/src/line.ts +++ b/packages/math/src/line.ts @@ -1,6 +1,6 @@ import { pointFrom } from "./point"; -import type { GlobalPoint, Line, LocalPoint } from "./types"; +import type { GenericPoint, Line } from "./types"; /** * Create a line from two points. @@ -8,7 +8,7 @@ import type { GlobalPoint, Line, LocalPoint } from "./types"; * @param points The two points lying on the line * @returns The line on which the points lie */ -export function line

(a: P, b: P): Line

{ +export function line

(a: P, b: P): Line

{ return [a, b] as Line

; } @@ -20,7 +20,7 @@ export function line

(a: P, b: P): Line

{ * @param b * @returns */ -export function linesIntersectAt( +export function linesIntersectAt( a: Line, b: Line, ): Point | null { diff --git a/packages/math/src/point.ts b/packages/math/src/point.ts index b6054a10a..db42a07a8 100644 --- a/packages/math/src/point.ts +++ b/packages/math/src/point.ts @@ -2,13 +2,7 @@ import { degreesToRadians } from "./angle"; import { PRECISION } from "./utils"; import { vectorFromPoint, vectorScale } from "./vector"; -import type { - LocalPoint, - GlobalPoint, - Radians, - Degrees, - Vector, -} from "./types"; +import type { Radians, Degrees, Vector, GenericPoint } from "./types"; /** * Create a properly typed Point instance from the X and Y coordinates. @@ -17,7 +11,7 @@ import type { * @param y The Y coordinate * @returns The branded and created point */ -export function pointFrom( +export function pointFrom( x: number, y: number, ): Point { @@ -30,7 +24,7 @@ export function pointFrom( * @param numberArray The number array to check and to convert to Point * @returns The point instance */ -export function pointFromArray( +export function pointFromArray( numberArray: number[], ): Point | undefined { return numberArray.length === 2 @@ -44,7 +38,7 @@ export function pointFromArray( * @param pair A number pair to convert to Point * @returns The point instance */ -export function pointFromPair( +export function pointFromPair( pair: [number, number], ): Point { return pair as Point; @@ -56,7 +50,7 @@ export function pointFromPair( * @param v The vector to convert * @returns The point the vector points at with origin 0,0 */ -export function pointFromVector

( +export function pointFromVector

( v: Vector, offset: P = pointFrom(0, 0), ): P { @@ -69,7 +63,7 @@ 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 GenericPoint { return ( Array.isArray(p) && p.length === 2 && @@ -88,7 +82,7 @@ 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( +export function pointsEqual( a: Point, b: Point, ): boolean { @@ -104,7 +98,7 @@ export function pointsEqual( * @param angle The radians to rotate the point by * @returns The rotated point */ -export function pointRotateRads( +export function pointRotateRads( [x, y]: Point, [cx, cy]: Point, angle: Radians, @@ -123,7 +117,7 @@ export function pointRotateRads( * @param angle The degree to rotate the point by * @returns The rotated point */ -export function pointRotateDegs( +export function pointRotateDegs( point: Point, center: Point, angle: Degrees, @@ -145,8 +139,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 GenericPoint, + To extends GenericPoint, >(p: From, v: Vector = [0, 0] as Vector): To { return pointFrom(p[0] + v[0], p[1] + v[1]); } @@ -158,7 +152,7 @@ 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 { +export function pointCenter

(a: P, b: P): P { return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); } @@ -169,10 +163,7 @@ export function pointCenter

(a: P, b: P): P { * @param b Second point * @returns The euclidean distance between the two points. */ -export function pointDistance

( - a: P, - b: P, -): number { +export function pointDistance

(a: P, b: P): number { return Math.hypot(b[0] - a[0], b[1] - a[1]); } @@ -185,10 +176,7 @@ 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

(a: P, b: P): number { const xDiff = b[0] - a[0]; const yDiff = b[1] - a[1]; @@ -203,7 +191,7 @@ export function pointDistanceSq

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

( +export const pointScaleFromOrigin =

( p: P, mid: P, multiplier: number, @@ -218,7 +206,7 @@ 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: P, q: P, r: P, diff --git a/packages/math/src/polygon.ts b/packages/math/src/polygon.ts index a50d4e853..8f1671a54 100644 --- a/packages/math/src/polygon.ts +++ b/packages/math/src/polygon.ts @@ -2,21 +2,17 @@ import { pointsEqual } from "./point"; import { lineSegment, pointOnLineSegment } from "./segment"; import { PRECISION } from "./utils"; -import type { GlobalPoint, LocalPoint, Polygon } from "./types"; +import type { GenericPoint, Polygon } from "./types"; -export function polygon( - ...points: Point[] -) { +export function polygon(...points: Point[]) { return polygonClose(points) as Polygon; } -export function polygonFromPoints( - points: Point[], -) { +export function polygonFromPoints(points: Point[]) { return polygonClose(points) as Polygon; } -export const polygonIncludesPoint = ( +export const polygonIncludesPoint = ( point: Point, polygon: Polygon, ) => { @@ -69,7 +65,7 @@ export const polygonIncludesPointNonZero = ( return windingNumber !== 0; }; -export const pointOnPolygon = ( +export const pointOnPolygon = ( p: Point, poly: Polygon, threshold = PRECISION, @@ -86,16 +82,12 @@ export const pointOnPolygon = ( return on; }; -function polygonClose( - polygon: Point[], -) { +function polygonClose(polygon: Point[]) { return polygonIsClosed(polygon) ? polygon : ([...polygon, polygon[0]] as Polygon); } -function polygonIsClosed( - polygon: Point[], -) { +function polygonIsClosed(polygon: Point[]) { return pointsEqual(polygon[0], polygon[polygon.length - 1]); } diff --git a/packages/math/src/rectangle.ts b/packages/math/src/rectangle.ts index 394b5c2f8..0675fee7e 100644 --- a/packages/math/src/rectangle.ts +++ b/packages/math/src/rectangle.ts @@ -1,18 +1,19 @@ import { pointFrom } from "./point"; import { lineSegment, lineSegmentIntersectionPoints } from "./segment"; -import type { GlobalPoint, LineSegment, LocalPoint, Rectangle } from "./types"; +import type { GenericPoint, LineSegment, Rectangle } from "./types"; -export function rectangle

( +export function rectangle

( topLeft: P, bottomRight: P, ): Rectangle

{ return [topLeft, bottomRight] as Rectangle

; } -export function rectangleIntersectLineSegment< - Point extends LocalPoint | GlobalPoint, ->(r: Rectangle, l: LineSegment): Point[] { +export function rectangleIntersectLineSegment( + r: Rectangle, + l: LineSegment, +): Point[] { return [ lineSegment(r[0], pointFrom(r[1][0], r[0][1])), lineSegment(pointFrom(r[1][0], r[0][1]), r[1]), diff --git a/packages/math/src/segment.ts b/packages/math/src/segment.ts index dade79039..90a818126 100644 --- a/packages/math/src/segment.ts +++ b/packages/math/src/segment.ts @@ -14,7 +14,7 @@ import { vectorSubtract, } from "./vector"; -import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types"; +import type { GenericPoint, LineSegment, Radians } from "./types"; /** * Create a line segment from two points. @@ -22,7 +22,7 @@ import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types"; * @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

{ @@ -34,7 +34,7 @@ export function lineSegment

( * @param segment * @returns */ -export const isLineSegment = ( +export const isLineSegment = ( segment: unknown, ): segment is LineSegment => Array.isArray(segment) && @@ -51,7 +51,7 @@ export const isLineSegment = ( * @param origin * @returns */ -export const lineSegmentRotate = ( +export const lineSegmentRotate = ( l: LineSegment, angle: Radians, origin?: Point, @@ -66,7 +66,7 @@ export const lineSegmentRotate = ( * Calculates the point two line segments with a definite start and end point * intersect at. */ -export const segmentsIntersectAt = ( +export const segmentsIntersectAt = ( a: Readonly>, b: Readonly>, ): Point | null => { @@ -99,7 +99,7 @@ export const segmentsIntersectAt = ( return null; }; -export const pointOnLineSegment = ( +export const pointOnLineSegment = ( point: Point, line: LineSegment, threshold = PRECISION, @@ -113,7 +113,7 @@ export const pointOnLineSegment = ( return distance < threshold; }; -export const distanceToLineSegment = ( +export const distanceToLineSegment = ( point: Point, line: LineSegment, ) => { @@ -158,9 +158,7 @@ export const distanceToLineSegment = ( * @param s * @returns */ -export function lineSegmentIntersectionPoints< - Point extends GlobalPoint | LocalPoint, ->( +export function lineSegmentIntersectionPoints( l: LineSegment, s: LineSegment, threshold?: number, diff --git a/packages/math/src/triangle.ts b/packages/math/src/triangle.ts index bc74372b7..8e6144e83 100644 --- a/packages/math/src/triangle.ts +++ b/packages/math/src/triangle.ts @@ -1,4 +1,4 @@ -import type { GlobalPoint, LocalPoint, Triangle } from "./types"; +import type { GenericPoint, Triangle } from "./types"; // Types @@ -11,7 +11,7 @@ import type { GlobalPoint, LocalPoint, Triangle } from "./types"; * @param p The point to test whether is in the triangle * @returns TRUE if the point is inside of the triangle */ -export function triangleIncludesPoint

( +export function triangleIncludesPoint

( [a, b, c]: Triangle

, p: P, ): boolean { diff --git a/packages/math/src/types.ts b/packages/math/src/types.ts index da7d5d6ab..28ed500a8 100644 --- a/packages/math/src/types.ts +++ b/packages/math/src/types.ts @@ -27,6 +27,12 @@ export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" }; // Point // +/** + * Represents a 2D position in world or canvas space. A + * unintified glboal, viewport, or local coordinate. + */ +export type GenericPoint = GlobalPoint | ViewportPoint | LocalPoint; + /** * Represents a 2D position in world or canvas space. A * global coordinate. @@ -35,6 +41,14 @@ export type GlobalPoint = [x: number, y: number] & { _brand: "excalimath__globalpoint"; }; +/** + * Represents a 2D position in world or viewport space. A + * viewport coordinate. + */ +export type ViewportPoint = [x: number, y: number] & { + _brand: "excalimath__viewportpoint"; +}; + /** * Represents a 2D position in whatever local space it's * needed. A local coordinate. @@ -48,7 +62,7 @@ export type LocalPoint = [x: number, y: number] & { /** * A line is an infinitely long object with no width, depth, or curvature. */ -export type Line

= [p: P, q: P] & { +export type Line

= [p: P, q: P] & { _brand: "excalimath_line"; }; @@ -57,7 +71,7 @@ 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"; }; @@ -77,18 +91,14 @@ export type Vector = [u: number, v: number] & { /** * A triangle represented by 3 points */ -export type Triangle

= [ - a: P, - b: P, - c: P, -] & { +export type Triangle

= [a: P, b: P, c: P] & { _brand: "excalimath__triangle"; }; /** * A rectangular shape represented by 4 points at its corners */ -export type Rectangle

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

= [a: P, b: P] & { _brand: "excalimath__rectangle"; }; @@ -100,7 +110,7 @@ export type Rectangle

= [a: P, b: P] & { * A polygon is a closed shape by connecting the given points * rectangles and diamonds are modelled by polygons */ -export type Polygon = Point[] & { +export type Polygon = Point[] & { _brand: "excalimath_polygon"; }; @@ -111,12 +121,7 @@ export type Polygon = Point[] & { /** * Cubic bezier curve with four control points */ -export type Curve = [ - Point, - Point, - Point, - Point, -] & { +export type Curve = [Point, Point, Point, Point] & { _brand: "excalimath_curve"; }; @@ -131,7 +136,7 @@ export type PolarCoords = [ 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; halfWidth: number; halfHeight: number; diff --git a/packages/math/src/vector.ts b/packages/math/src/vector.ts index 246722067..b9d214288 100644 --- a/packages/math/src/vector.ts +++ b/packages/math/src/vector.ts @@ -1,4 +1,4 @@ -import type { GlobalPoint, LocalPoint, Vector } from "./types"; +import type { GenericPoint, Vector } from "./types"; /** * Create a vector from the x and y coordiante elements. @@ -23,7 +23,7 @@ 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( +export function vectorFromPoint( p: Point, origin: Point = [0, 0] as Point, ): Vector { diff --git a/packages/utils/src/bbox.ts b/packages/utils/src/bbox.ts index a56128156..1fb96f656 100644 --- a/packages/utils/src/bbox.ts +++ b/packages/utils/src/bbox.ts @@ -1,17 +1,14 @@ import { vectorCross, vectorFromPoint, - type GlobalPoint, - type LocalPoint, + type GenericPoint, } from "@excalidraw/math"; import type { Bounds } from "@excalidraw/element/bounds"; -export type LineSegment

= [P, P]; +export type LineSegment

= [P, P]; -export function getBBox

( - line: LineSegment

, -): Bounds { +export function getBBox

(line: LineSegment

): Bounds { return [ Math.min(line[0][0], line[1][0]), Math.min(line[0][1], line[1][1]), @@ -26,10 +23,7 @@ export function doBBoxesIntersect(a: Bounds, b: Bounds) { const EPSILON = 0.000001; -export function isPointOnLine

( - l: LineSegment

, - p: P, -) { +export function isPointOnLine

(l: LineSegment

, p: P) { const p1 = vectorFromPoint(l[1], l[0]); const p2 = vectorFromPoint(p, l[0]); @@ -38,7 +32,7 @@ export function isPointOnLine

( return Math.abs(r) < EPSILON; } -export function isPointRightOfLine

( +export function isPointRightOfLine

( l: LineSegment

, p: P, ) { @@ -48,9 +42,10 @@ export function isPointRightOfLine

( return vectorCross(p1, p2) < 0; } -export function isLineSegmentTouchingOrCrossingLine< - P extends GlobalPoint | LocalPoint, ->(a: LineSegment

, b: LineSegment

) { +export function isLineSegmentTouchingOrCrossingLine

( + a: LineSegment

, + b: LineSegment

, +) { return ( isPointOnLine(a, b[0]) || isPointOnLine(a, b[1]) || @@ -61,7 +56,7 @@ export function isLineSegmentTouchingOrCrossingLine< } // https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/ -export function doLineSegmentsIntersect

( +export function doLineSegmentsIntersect

( a: LineSegment

, b: LineSegment

, ) { diff --git a/packages/utils/src/collision.ts b/packages/utils/src/collision.ts index b7c155f66..207be9bee 100644 --- a/packages/utils/src/collision.ts +++ b/packages/utils/src/collision.ts @@ -5,19 +5,17 @@ import { pointOnLineSegment, pointOnPolygon, polygonFromPoints, - type GlobalPoint, - type LocalPoint, type Polygon, } from "@excalidraw/math"; -import type { Curve } from "@excalidraw/math"; +import type { Curve, GenericPoint } from "@excalidraw/math"; import { pointInEllipse, pointOnEllipse } from "./shape"; import type { Polycurve, Polyline, GeometricShape } from "./shape"; // check if the given point is considered on the given shape's border -export const isPointOnShape = ( +export const isPointOnShape = ( point: Point, shape: GeometricShape, tolerance = 0, @@ -43,7 +41,7 @@ export const isPointOnShape = ( }; // check if the given point is considered inside the element's border -export const isPointInShape = ( +export const isPointInShape = ( point: Point, shape: GeometricShape, ) => { @@ -69,14 +67,14 @@ export const isPointInShape = ( }; // check if the given element is in the given bounds -export const isPointInBounds = ( +export const isPointInBounds = ( point: Point, bounds: Polygon, ) => { return polygonIncludesPoint(point, bounds); }; -const pointOnPolycurve = ( +const pointOnPolycurve = ( point: Point, polycurve: Polycurve, tolerance: number, @@ -84,7 +82,7 @@ const pointOnPolycurve = ( return polycurve.some((curve) => pointOnCurve(point, curve, tolerance)); }; -const cubicBezierEquation = ( +const cubicBezierEquation = ( curve: Curve, ) => { const [p0, p1, p2, p3] = curve; @@ -96,7 +94,7 @@ const cubicBezierEquation = ( p0[idx] * Math.pow(t, 3); }; -const polyLineFromCurve = ( +const polyLineFromCurve = ( curve: Curve, segments = 10, ): Polyline => { @@ -118,7 +116,7 @@ const polyLineFromCurve = ( return lineSegments; }; -export const pointOnCurve = ( +export const pointOnCurve = ( point: Point, curve: Curve, threshold: number, @@ -126,7 +124,7 @@ export const pointOnCurve = ( return pointOnPolyline(point, polyLineFromCurve(curve), threshold); }; -export const pointOnPolyline = ( +export const pointOnPolyline = ( point: Point, polyline: Polyline, threshold = 10e-5, diff --git a/packages/utils/src/shape.ts b/packages/utils/src/shape.ts index b750c232e..1cc332e7f 100644 --- a/packages/utils/src/shape.ts +++ b/packages/utils/src/shape.ts @@ -30,8 +30,6 @@ import { vectorAdd, vectorFromPoint, vectorScale, - type GlobalPoint, - type LocalPoint, } from "@excalidraw/math"; import { getElementAbsoluteCoords } from "@excalidraw/element/bounds"; @@ -52,31 +50,36 @@ import type { ExcalidrawSelectionElement, ExcalidrawTextElement, } from "@excalidraw/element/types"; -import type { Curve, LineSegment, Polygon, Radians } from "@excalidraw/math"; +import type { + Curve, + GenericPoint, + LineSegment, + Polygon, + Radians, +} from "@excalidraw/math"; import type { Drawable, Op } from "roughjs/bin/core"; // 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 = - LineSegment[]; +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 = | { type: "line"; data: LineSegment; @@ -113,7 +116,7 @@ type RectangularElement = | ExcalidrawSelectionElement; // polygon -export const getPolygonShape = ( +export const getPolygonShape = ( element: RectangularElement, ): GeometricShape => { const { angle, width, height, x, y } = element; @@ -148,7 +151,7 @@ export const getPolygonShape = ( }; // return the selection box for an element, possibly rotated as well -export const getSelectionBoxShape = ( +export const getSelectionBoxShape = ( element: ExcalidrawElement, elementsMap: ElementsMap, padding = 10, @@ -178,7 +181,7 @@ export const getSelectionBoxShape = ( }; // ellipse -export const getEllipseShape = ( +export const getEllipseShape = ( element: ExcalidrawEllipseElement, ): GeometricShape => { const { width, height, angle, x, y } = element; @@ -209,7 +212,7 @@ export const getCurvePathOps = (shape: Drawable): Op[] => { }; // linear -export const getCurveShape = ( +export const getCurveShape = ( roughShape: Drawable, startingPoint: Point = pointFrom(0, 0), angleInRadian: Radians, @@ -247,7 +250,7 @@ export const getCurveShape = ( }; }; -const polylineFromPoints = ( +const polylineFromPoints = ( points: Point[], ): Polyline => { let previousPoint: Point = points[0]; @@ -262,7 +265,7 @@ const polylineFromPoints = ( return polyline; }; -export const getFreedrawShape = ( +export const getFreedrawShape = ( element: ExcalidrawFreeDrawElement, center: Point, isClosed: boolean = false, @@ -293,7 +296,7 @@ export const getFreedrawShape = ( ) as GeometricShape; }; -export const getClosedCurveShape = ( +export const getClosedCurveShape = ( element: ExcalidrawLinearElement, roughShape: Drawable, startingPoint: Point = pointFrom(0, 0), @@ -359,9 +362,7 @@ export const getClosedCurveShape = ( * @returns An array of intersections */ // TODO: Replace with final rounded rectangle code -export const segmentIntersectRectangleElement = < - Point extends LocalPoint | GlobalPoint, ->( +export const segmentIntersectRectangleElement = ( element: ExcalidrawBindableElement, segment: LineSegment, gap: number = 0, @@ -399,7 +400,7 @@ export const segmentIntersectRectangleElement = < .filter((i): i is Point => !!i); }; -const distanceToEllipse = ( +const distanceToEllipse = ( p: Point, ellipse: Ellipse, ) => { @@ -456,7 +457,7 @@ const distanceToEllipse = ( ); }; -export const pointOnEllipse = ( +export const pointOnEllipse = ( point: Point, ellipse: Ellipse, threshold = PRECISION, @@ -464,7 +465,7 @@ export const pointOnEllipse = ( return distanceToEllipse(point, ellipse) <= threshold; }; -export const pointInEllipse = ( +export const pointInEllipse = ( p: Point, ellipse: Ellipse, ) => { @@ -486,7 +487,7 @@ export const pointInEllipse = ( ); }; -export const ellipseAxes = ( +export const ellipseAxes = ( ellipse: Ellipse, ) => { const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight; @@ -504,7 +505,7 @@ export const ellipseAxes = ( }; }; -export const ellipseFocusToCenter = ( +export const ellipseFocusToCenter = ( ellipse: Ellipse, ) => { const { majorAxis, minorAxis } = ellipseAxes(ellipse); @@ -512,7 +513,7 @@ export const ellipseFocusToCenter = ( return Math.sqrt(majorAxis ** 2 - minorAxis ** 2); }; -export const ellipseExtremes = ( +export const ellipseExtremes = ( ellipse: Ellipse, ) => { const { center, angle } = ellipse; diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 91108a600..1bf6cdaa2 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -69,10 +69,10 @@ exports[`exportToSvg > with default arguments 1`] = ` "openMenu": null, "openPopup": null, "openSidebar": null, - "originSnapOffset": { - "x": 0, - "y": 0, - }, + "originSnapOffset": [ + 0, + 0, + ], "pasteDialog": { "data": null, "shown": false,