refactor: Replace { x: number, y: number } type declarations with Point type

- Updated type declarations written as { x: number, y: number } to use the Point type as per requirements.
- Explicit names were used for variables by destructuring previously used object types.
- For example, scenePointer.x, scenePointer.y are now destructured as scenePointerX and scenePointerY.
- When a Point type was required as an argument, the `pointFrom` helper function was used to convert and pass the value.
This commit is contained in:
sunub 2025-04-17 13:22:59 +09:00
parent 85cb973936
commit 899c652147
21 changed files with 210 additions and 188 deletions

View file

@ -422,7 +422,7 @@ export default function ExampleApp({
if (!excalidrawAPI) { if (!excalidrawAPI) {
return false; return false;
} }
const { x, y } = viewportCoordsToSceneCoords( const [x, y] = viewportCoordsToSceneCoords(
{ {
clientX: event.clientX - pointerDownState.hitElementOffsets.x, clientX: event.clientX - pointerDownState.hitElementOffsets.x,
clientY: event.clientY - pointerDownState.hitElementOffsets.y, clientY: event.clientY - pointerDownState.hitElementOffsets.y,

View file

@ -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 { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
@ -448,11 +454,11 @@ export const viewportCoordsToSceneCoords = (
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
}, },
) => { ): ViewportPoint => {
const x = (clientX - offsetLeft) / zoom.value - scrollX; const x = (clientX - offsetLeft) / zoom.value - scrollX;
const y = (clientY - offsetTop) / zoom.value - scrollY; const y = (clientY - offsetTop) / zoom.value - scrollY;
return { x, y }; return pointFrom<ViewportPoint>(x, y);
}; };
export const sceneCoordsToViewportCoords = ( export const sceneCoordsToViewportCoords = (
@ -470,10 +476,10 @@ export const sceneCoordsToViewportCoords = (
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
}, },
) => { ): ViewportPoint => {
const x = (sceneX + scrollX) * zoom.value + offsetLeft; const x = (sceneX + scrollX) * zoom.value + offsetLeft;
const y = (sceneY + scrollY) * zoom.value + offsetTop; const y = (sceneY + scrollY) * zoom.value + offsetTop;
return { x, y }; return pointFrom<ViewportPoint>(x, y);
}; };
export const getGlobalCSSVariable = (name: string) => 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 isRTL = (text: string) => RE_RTL_CHECK.test(text);
export const tupleToCoors = ( export const tupleToCoors = <Point extends GenericPoint>(
xyTuple: readonly [number, number], xyTuple: readonly [number, number],
): { x: number; y: number } => { ): Point => {
const [x, y] = xyTuple; const [x, y] = xyTuple;
return { x, y }; return pointFrom(x, y);
}; };
/** use as a rejectionHandler to mute filesystem Abort errors */ /** use as a rejectionHandler to mute filesystem Abort errors */

View file

@ -29,7 +29,7 @@ import {
import { isPointOnShape } from "@excalidraw/utils/collision"; 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"; import type Scene from "@excalidraw/excalidraw/scene/Scene";
@ -426,10 +426,10 @@ export const getSuggestedBindingsForArrows = (
); );
}; };
export const maybeBindLinearElement = ( export const maybeBindLinearElement = <Point extends GenericPoint>(
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState, appState: AppState,
pointerCoords: { x: number; y: number }, pointerCoords: Point,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
): void => { ): void => {
@ -577,11 +577,8 @@ const unbindLinearElement = (
return binding.elementId; return binding.elementId;
}; };
export const getHoveredElementForBinding = ( export const getHoveredElementForBinding = <Point extends GenericPoint>(
pointerCoords: { pointerCoords: Point,
x: number;
y: number;
},
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
@ -1393,11 +1390,11 @@ const getElligibleElementForBindingElement = (
); );
}; };
const getLinearElementEdgeCoors = ( const getLinearElementEdgeCoors = <Point extends GenericPoint>(
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
): { x: number; y: number } => { ): Point => {
const index = startOrEnd === "start" ? 0 : -1; const index = startOrEnd === "start" ? 0 : -1;
return tupleToCoors( return tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
@ -1706,9 +1703,9 @@ const newBoundElements = (
return nextBoundElements; return nextBoundElements;
}; };
export const bindingBorderTest = ( export const bindingBorderTest = <Point extends GenericPoint>(
element: NonDeleted<ExcalidrawBindableElement>, element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number }, [x, y]: Point,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
fullShape?: boolean, fullShape?: boolean,

View file

@ -18,6 +18,7 @@ import { pointsOnBezierCurves } from "points-on-curve";
import type { import type {
Curve, Curve,
Degrees, Degrees,
GenericPoint,
GlobalPoint, GlobalPoint,
LineSegment, LineSegment,
LocalPoint, LocalPoint,
@ -1051,7 +1052,7 @@ export const getElementPointsCoords = (
export const getClosestElementBounds = ( export const getClosestElementBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
from: { x: number; y: number }, from: GenericPoint,
): Bounds => { ): Bounds => {
if (!elements.length) { if (!elements.length) {
return [0, 0, 0, 0]; return [0, 0, 0, 0];
@ -1064,7 +1065,7 @@ export const getClosestElementBounds = (
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
const distance = pointDistance( const distance = pointDistance(
pointFrom((x1 + x2) / 2, (y1 + y2) / 2), pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
pointFrom(from.x, from.y), from,
); );
if (distance < minDistance) { if (distance < minDistance) {

View file

@ -12,6 +12,7 @@ import type {
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene"; import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { GenericPoint } from "@excalidraw/math";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
@ -208,10 +209,7 @@ export const dragNewElement = ({
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */ true */
widthAspectRatio?: number | null; widthAspectRatio?: number | null;
originOffset?: { originOffset?: GenericPoint | null;
x: number;
y: number;
} | null;
informMutation?: boolean; informMutation?: boolean;
}) => { }) => {
if (shouldMaintainAspectRatio && newElement.type !== "selection") { if (shouldMaintainAspectRatio && newElement.type !== "selection") {
@ -285,11 +283,12 @@ export const dragNewElement = ({
}; };
} }
const [originOffsetX, originOffsetY] = originOffset ?? [0, 0];
mutateElement( mutateElement(
newElement, newElement,
{ {
x: newX + (originOffset?.x ?? 0), x: newX + originOffsetX,
y: newY + (originOffset?.y ?? 0), y: newY + originOffsetY,
width, width,
height, height,
...textAutoResize, ...textAutoResize,

View file

@ -1,5 +1,9 @@
import { arrayToMap } from "@excalidraw/common"; 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 { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds"; import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
@ -154,11 +158,8 @@ export const elementOverlapsWithFrame = (
); );
}; };
export const isCursorInFrame = ( export const isCursorInFrame = <Point extends GenericPoint>(
cursorCoords: { cursorCoords: Point,
x: number;
y: number;
},
frame: NonDeleted<ExcalidrawFrameLikeElement>, frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
@ -166,7 +167,7 @@ export const isCursorInFrame = (
return isPointWithinBounds( return isPointWithinBounds(
pointFrom(fx1, fy1), pointFrom(fx1, fy1),
pointFrom(cursorCoords.x, cursorCoords.y), cursorCoords,
pointFrom(fx2, fy2), pointFrom(fx2, fy2),
); );
}; };

View file

@ -3,8 +3,6 @@ import {
pointFrom, pointFrom,
pointRotateRads, pointRotateRads,
pointsEqual, pointsEqual,
type GlobalPoint,
type LocalPoint,
pointDistance, pointDistance,
vectorFromPoint, vectorFromPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
@ -26,7 +24,12 @@ import Scene from "@excalidraw/excalidraw/scene/Scene";
import type { Store } from "@excalidraw/excalidraw/store"; import type { Store } from "@excalidraw/excalidraw/store";
import type { Radians } from "@excalidraw/math"; import type {
GlobalPoint,
LocalPoint,
GenericPoint,
Radians,
} from "@excalidraw/math";
import type { import type {
AppState, AppState,
@ -106,7 +109,7 @@ export class LinearElementEditor {
/** index */ /** index */
lastClickedPoint: number; lastClickedPoint: number;
lastClickedIsEndPoint: boolean; lastClickedIsEndPoint: boolean;
origin: Readonly<{ x: number; y: number }> | null; origin: Readonly<GenericPoint> | null;
segmentMidpoint: { segmentMidpoint: {
value: GlobalPoint | null; value: GlobalPoint | null;
index: number | null; index: number | null;
@ -117,7 +120,7 @@ export class LinearElementEditor {
/** whether you're dragging a point */ /** whether you're dragging a point */
public readonly isDragging: boolean; public readonly isDragging: boolean;
public readonly lastUncommittedPoint: LocalPoint | null; public readonly lastUncommittedPoint: LocalPoint | null;
public readonly pointerOffset: Readonly<{ x: number; y: number }>; public readonly pointerOffset: Readonly<GenericPoint>;
public readonly startBindingElement: public readonly startBindingElement:
| ExcalidrawBindableElement | ExcalidrawBindableElement
| null | null
@ -139,7 +142,7 @@ export class LinearElementEditor {
this.selectedPointsIndices = null; this.selectedPointsIndices = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
this.isDragging = false; this.isDragging = false;
this.pointerOffset = { x: 0, y: 0 }; this.pointerOffset = pointFrom(0, 0);
this.startBindingElement = "keep"; this.startBindingElement = "keep";
this.endBindingElement = "keep"; this.endBindingElement = "keep";
this.pointerDownState = { this.pointerDownState = {
@ -242,14 +245,14 @@ export class LinearElementEditor {
/** /**
* @returns whether point was dragged * @returns whether point was dragged
*/ */
static handlePointDragging( static handlePointDragging<Point extends GenericPoint>(
event: PointerEvent, event: PointerEvent,
app: AppClassProperties, app: AppClassProperties,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
maybeSuggestBinding: ( maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[], pointSceneCoords: Point[],
) => void, ) => void,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scene: Scene, scene: Scene,
@ -320,11 +323,13 @@ export class LinearElementEditor {
}, },
]); ]);
} else { } else {
const [pointerOffsetX, pointerOffsetY] =
linearElementEditor.pointerOffset;
const newDraggingPointPosition = LinearElementEditor.createPointAt( const newDraggingPointPosition = LinearElementEditor.createPointAt(
element, element,
elementsMap, elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - pointerOffsetX,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - pointerOffsetY,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
@ -339,8 +344,8 @@ export class LinearElementEditor {
? LinearElementEditor.createPointAt( ? LinearElementEditor.createPointAt(
element, element,
elementsMap, elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - pointerOffsetX,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - pointerOffsetY,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
) )
: pointFrom( : pointFrom(
@ -363,7 +368,7 @@ export class LinearElementEditor {
// suggest bindings for first and last point if selected // suggest bindings for first and last point if selected
if (isBindingElement(element, false)) { if (isBindingElement(element, false)) {
const coords: { x: number; y: number }[] = []; const coords: Point[] = [];
const firstSelectedIndex = selectedPointsIndices[0]; const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) { if (firstSelectedIndex === 0) {
@ -511,7 +516,7 @@ export class LinearElementEditor {
? [pointerDownState.lastClickedPoint] ? [pointerDownState.lastClickedPoint]
: selectedPointsIndices, : selectedPointsIndices,
isDragging: false, isDragging: false,
pointerOffset: { x: 0, y: 0 }, pointerOffset: pointFrom(0, 0),
}; };
} }
@ -586,9 +591,9 @@ export class LinearElementEditor {
editorMidPointsCache.zoom = appState.zoom.value; editorMidPointsCache.zoom = appState.zoom.value;
}; };
static getSegmentMidpointHitCoords = ( static getSegmentMidpointHitCoords = <Point extends GenericPoint>(
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number }, [scenePointerX, scenePointerY]: Point,
appState: AppState, appState: AppState,
elementsMap: ElementsMap, elementsMap: ElementsMap,
): GlobalPoint | null => { ): GlobalPoint | null => {
@ -601,8 +606,8 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointerX,
scenePointer.y, scenePointerY,
); );
if (!isElbowArrow(element) && clickedPointIndex >= 0) { if (!isElbowArrow(element) && clickedPointIndex >= 0) {
return null; return null;
@ -630,7 +635,7 @@ export class LinearElementEditor {
existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[0],
existingSegmentMidpointHitCoords[1], existingSegmentMidpointHitCoords[1],
), ),
pointFrom(scenePointer.x, scenePointer.y), pointFrom(scenePointerX, scenePointerY),
); );
if (distance <= threshold) { if (distance <= threshold) {
return existingSegmentMidpointHitCoords; return existingSegmentMidpointHitCoords;
@ -644,7 +649,7 @@ export class LinearElementEditor {
if (midPoints[index] !== null) { if (midPoints[index] !== null) {
const distance = pointDistance( const distance = pointDistance(
midPoints[index]!, midPoints[index]!,
pointFrom(scenePointer.x, scenePointer.y), pointFrom(scenePointerX, scenePointerY),
); );
if (distance <= threshold) { if (distance <= threshold) {
return midPoints[index]; return midPoints[index];
@ -656,7 +661,7 @@ export class LinearElementEditor {
return null; return null;
}; };
static isSegmentTooShort<P extends GlobalPoint | LocalPoint>( static isSegmentTooShort<P extends GenericPoint>(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
startPoint: P, startPoint: P,
endPoint: P, endPoint: P,
@ -747,11 +752,11 @@ export class LinearElementEditor {
return -1; return -1;
} }
static handlePointerDown( static handlePointerDown<Point extends GenericPoint>(
event: React.PointerEvent<HTMLElement>, event: React.PointerEvent<HTMLElement>,
app: AppClassProperties, app: AppClassProperties,
store: Store, store: Store,
scenePointer: { x: number; y: number }, scenePointer: Point,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scene: Scene, scene: Scene,
): { ): {
@ -762,6 +767,7 @@ export class LinearElementEditor {
const appState = app.state; const appState = app.state;
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements(); const elements = scene.getNonDeletedElements();
const [scenePointerX, scenePointerY] = scenePointer;
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = { const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
didAddPoint: false, didAddPoint: false,
@ -801,8 +807,8 @@ export class LinearElementEditor {
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
element, element,
elementsMap, elementsMap,
scenePointer.x, scenePointerX,
scenePointer.y, scenePointerY,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
), ),
], ],
@ -816,7 +822,7 @@ export class LinearElementEditor {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1, lastClickedPoint: -1,
lastClickedIsEndPoint: false, lastClickedIsEndPoint: false,
origin: { x: scenePointer.x, y: scenePointer.y }, origin: pointFrom(scenePointerX, scenePointerY),
segmentMidpoint: { segmentMidpoint: {
value: segmentMidpoint, value: segmentMidpoint,
index: segmentMidpointIndex, index: segmentMidpointIndex,
@ -842,8 +848,8 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointerX,
scenePointer.y, scenePointerY,
); );
// if we clicked on a point, set the element as hitElement otherwise // if we clicked on a point, set the element as hitElement otherwise
// it would get deselected if the point is outside the hitbox area // it would get deselected if the point is outside the hitbox area
@ -897,7 +903,7 @@ export class LinearElementEditor {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices, prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: clickedPointIndex, lastClickedPoint: clickedPointIndex,
lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1, lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1,
origin: { x: scenePointer.x, y: scenePointer.y }, origin: pointFrom(scenePointerX, scenePointerY),
segmentMidpoint: { segmentMidpoint: {
value: segmentMidpoint, value: segmentMidpoint,
index: segmentMidpointIndex, index: segmentMidpointIndex,
@ -906,17 +912,17 @@ export class LinearElementEditor {
}, },
selectedPointsIndices: nextSelectedPointsIndices, selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint pointerOffset: targetPoint
? { ? pointFrom(
x: scenePointer.x - targetPoint[0], scenePointerX - targetPoint[0],
y: scenePointer.y - targetPoint[1], scenePointerY - targetPoint[1],
} )
: { x: 0, y: 0 }, : pointFrom(0, 0),
}; };
return ret; return ret;
} }
static arePointsEqual<Point extends LocalPoint | GlobalPoint>( static arePointsEqual<Point extends GenericPoint>(
point1: Point | null, point1: Point | null,
point2: Point | null, point2: Point | null,
) { ) {
@ -977,11 +983,13 @@ export class LinearElementEditor {
height + lastCommittedPoint[1], height + lastCommittedPoint[1],
); );
} else { } else {
const [pointerOffsetX, pointerOffsetY] =
appState.editingLinearElement.pointerOffset;
newPoint = LinearElementEditor.createPointAt( newPoint = LinearElementEditor.createPointAt(
element, element,
elementsMap, elementsMap,
scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerX - pointerOffsetX,
scenePointerY - appState.editingLinearElement.pointerOffset.y, scenePointerY - pointerOffsetY,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element) event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
? null ? null
: app.getEffectiveGridSize(), : app.getEffectiveGridSize(),
@ -1376,10 +1384,7 @@ export class LinearElementEditor {
} }
const origin = linearElementEditor.pointerDownState.origin!; const origin = linearElementEditor.pointerDownState.origin!;
const dist = pointDistance( const dist = pointDistance(origin, pointerCoords);
pointFrom(origin.x, origin.y),
pointFrom(pointerCoords.x, pointerCoords.y),
);
if ( if (
!appState.editingLinearElement && !appState.editingLinearElement &&
dist < DRAGGING_THRESHOLD / appState.zoom.value dist < DRAGGING_THRESHOLD / appState.zoom.value
@ -1412,11 +1417,12 @@ export class LinearElementEditor {
selectedPointsIndices: linearElementEditor.selectedPointsIndices, selectedPointsIndices: linearElementEditor.selectedPointsIndices,
}; };
const [pointerX, pointerY] = pointerCoords;
const midpoint = LinearElementEditor.createPointAt( const midpoint = LinearElementEditor.createPointAt(
element, element,
elementsMap, elementsMap,
pointerCoords.x, pointerX,
pointerCoords.y, pointerY,
snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null, snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null,
); );
const points = [ const points = [
@ -1528,31 +1534,28 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap, elementsMap: ElementsMap,
referencePoint: LocalPoint, referencePoint: LocalPoint,
scenePointer: GlobalPoint, [scenePointerX, scenePointerY]: GlobalPoint,
gridSize: NullableGridSize, gridSize: NullableGridSize,
) { ) {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( const [referencePointCoordsX, referencePointCoordsY] =
element, LinearElementEditor.getPointGlobalCoordinates(
referencePoint, element,
elementsMap, referencePoint,
); elementsMap,
);
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
return [ return [
scenePointer[0] - referencePointCoords[0], scenePointerX - referencePointCoordsX,
scenePointer[1] - referencePointCoords[1], scenePointerY - referencePointCoordsY,
]; ];
} }
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(scenePointerX, scenePointerY, gridSize);
scenePointer[0],
scenePointer[1],
gridSize,
);
const { width, height } = getLockedLinearCursorAlignSize( const { width, height } = getLockedLinearCursorAlignSize(
referencePointCoords[0], referencePointCoordsX,
referencePointCoords[1], referencePointCoordsY,
gridX, gridX,
gridY, gridY,
); );

View file

@ -37,26 +37,28 @@ export const isElementInViewport = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords( const [topLeftSceneCoordsX, topLeftSceneCoordsY] =
{ viewportCoordsToSceneCoords(
clientX: viewTransformations.offsetLeft, {
clientY: viewTransformations.offsetTop, clientX: viewTransformations.offsetLeft,
}, clientY: viewTransformations.offsetTop,
viewTransformations, },
); viewTransformations,
const bottomRightSceneCoords = viewportCoordsToSceneCoords( );
{ const [bottomRightSceneCoordsX, bottomRightSceneCoordsY] =
clientX: viewTransformations.offsetLeft + width, viewportCoordsToSceneCoords(
clientY: viewTransformations.offsetTop + height, {
}, clientX: viewTransformations.offsetLeft + width,
viewTransformations, clientY: viewTransformations.offsetTop + height,
); },
viewTransformations,
);
return ( return (
topLeftSceneCoords.x <= x2 && topLeftSceneCoordsX <= x2 &&
topLeftSceneCoords.y <= y2 && topLeftSceneCoordsY <= y2 &&
bottomRightSceneCoords.x >= x1 && bottomRightSceneCoordsX >= x1 &&
bottomRightSceneCoords.y >= y1 bottomRightSceneCoordsY >= y1
); );
}; };
@ -75,26 +77,29 @@ export const isElementCompletelyInViewport = (
padding?: Offsets, padding?: Offsets,
) => { ) => {
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords( const [topLeftSceneCoordsX, topLeftSceneCoordsY] =
{ viewportCoordsToSceneCoords(
clientX: viewTransformations.offsetLeft + (padding?.left || 0), {
clientY: viewTransformations.offsetTop + (padding?.top || 0), clientX: viewTransformations.offsetLeft + (padding?.left || 0),
}, clientY: viewTransformations.offsetTop + (padding?.top || 0),
viewTransformations, },
); viewTransformations,
const bottomRightSceneCoords = viewportCoordsToSceneCoords( );
{ const [bottomRightSceneCoordsX, bottomRightSceneCoordsY] =
clientX: viewTransformations.offsetLeft + width - (padding?.right || 0), viewportCoordsToSceneCoords(
clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0), {
}, clientX: viewTransformations.offsetLeft + width - (padding?.right || 0),
viewTransformations, clientY:
); viewTransformations.offsetTop + height - (padding?.bottom || 0),
},
viewTransformations,
);
return ( return (
x1 >= topLeftSceneCoords.x && x1 >= topLeftSceneCoordsX &&
y1 >= topLeftSceneCoords.y && y1 >= topLeftSceneCoordsY &&
x2 <= bottomRightSceneCoords.x && x2 <= bottomRightSceneCoordsX &&
y2 <= bottomRightSceneCoords.y y2 <= bottomRightSceneCoordsY
); );
}; };

View file

@ -1,4 +1,4 @@
import { clamp, roundToStep } from "@excalidraw/math"; import { clamp, pointFrom, roundToStep } from "@excalidraw/math";
import { import {
DEFAULT_CANVAS_BACKGROUND_PICKS, DEFAULT_CANVAS_BACKGROUND_PICKS,
@ -333,7 +333,7 @@ export const zoomToFitBounds = ({
); );
const centerScroll = centerScrollOn({ const centerScroll = centerScrollOn({
scenePoint: { x: centerX, y: centerY }, scenePoint: pointFrom(centerX, centerY),
viewportDimensions: { viewportDimensions: {
width: appState.width, width: appState.width,
height: appState.height, height: appState.height,

View file

@ -143,7 +143,7 @@ export const actionFinalize = register({
maybeBindLinearElement( maybeBindLinearElement(
multiPointElement, multiPointElement,
appState, appState,
{ x, y }, pointFrom(x, y),
elementsMap, elementsMap,
elements, elements,
); );

View file

@ -182,12 +182,12 @@ export class AnimatedTrail implements Trail {
const _stroke = trail const _stroke = trail
.getStrokeOutline(trail.options.size / state.zoom.value) .getStrokeOutline(trail.options.size / state.zoom.value)
.map(([x, y]) => { .map(([x, y]) => {
const result = sceneCoordsToViewportCoords( const [resultX, resultY] = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y }, { sceneX: x, sceneY: y },
state, state,
); );
return [result.x, result.y]; return [resultX, resultY];
}); });
const stroke = this.trailAnimation const stroke = this.trailAnimation

View file

@ -12,6 +12,8 @@ import {
DEFAULT_GRID_STEP, DEFAULT_GRID_STEP,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math";
import type { AppState, NormalizedZoomValue } from "./types"; import type { AppState, NormalizedZoomValue } from "./types";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio) const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
@ -112,10 +114,7 @@ export const getDefaultAppState = (): Omit<
showHyperlinkPopup: false, showHyperlinkPopup: false,
selectedLinearElement: null, selectedLinearElement: null,
snapLines: [], snapLines: [],
originSnapOffset: { originSnapOffset: pointFrom(0, 0),
x: 0,
y: 0,
},
objectsSnapModeEnabled: false, objectsSnapModeEnabled: false,
userToFollow: null, userToFollow: null,
followedBy: new Set(), followedBy: new Set(),

View file

@ -69,7 +69,7 @@ export const renderRemoteCursors = ({
}) => { }) => {
// Paint remote pointers // Paint remote pointers
for (const [socketId, pointer] of renderConfig.remotePointerViewportCoords) { for (const [socketId, pointer] of renderConfig.remotePointerViewportCoords) {
let { x, y } = pointer; let [x, y] = pointer;
const collaborator = appState.collaborators.get(socketId); const collaborator = appState.collaborators.get(socketId);

View file

@ -20,7 +20,7 @@ const getContainerCoords = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const [viewportX, viewportY] = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width, sceneY: y1 }, { sceneX: x1 + element.width, sceneY: y1 },
appState, appState,
); );

View file

@ -166,9 +166,14 @@ export const EyeDropper: React.FC<{
eyeDropperContainer.focus(); eyeDropperContainer.focus();
// init color preview else it would show only after the first mouse move // init color preview else it would show only after the first mouse move
const [
stablePropsAppLastViewportPositionX,
stablePropsAppLastViewportPositionY,
] = stableProps.app.lastViewportPosition;
mouseMoveListener({ mouseMoveListener({
clientX: stableProps.app.lastViewportPosition.x, clientX: stablePropsAppLastViewportPositionX,
clientY: stableProps.app.lastViewportPosition.y, clientY: stablePropsAppLastViewportPositionY,
altKey: false, altKey: false,
}); });

View file

@ -360,7 +360,7 @@ const getCoordsForPopover = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const [viewportX, viewportY] = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 }, { sceneX: x1 + element.width / 2, sceneY: y1 },
appState, appState,
); );
@ -422,7 +422,7 @@ const renderTooltip = (
appState, appState,
); );
const linkViewportCoords = sceneCoordsToViewportCoords( const [linkViewportCoordX, linkViewportCoordY] = sceneCoordsToViewportCoords(
{ sceneX: linkX, sceneY: linkY }, { sceneX: linkX, sceneY: linkY },
appState, appState,
); );
@ -430,8 +430,8 @@ const renderTooltip = (
updateTooltipPosition( updateTooltipPosition(
tooltipDiv, tooltipDiv,
{ {
left: linkViewportCoords.x, left: linkViewportCoordX,
top: linkViewportCoords.y, top: linkViewportCoordY,
width: linkWidth, width: linkWidth,
height: linkHeight, height: linkHeight,
}, },
@ -457,7 +457,7 @@ const shouldHideLinkPopup = (
appState: AppState, appState: AppState,
[clientX, clientY]: GlobalPoint, [clientX, clientY]: GlobalPoint,
): Boolean => { ): Boolean => {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( const [sceneX, sceneY] = viewportCoordsToSceneCoords(
{ clientX, clientY }, { clientX, clientY },
appState, appState,
); );

View file

@ -1,15 +1,25 @@
import type { PointerCoords } from "./types"; import { pointFrom, type GenericPoint } from "@excalidraw/math";
export const getCenter = (pointers: Map<number, PointerCoords>) => { export const getCenter = <Point extends GenericPoint>(
pointers: Map<number, Point>,
): Point => {
const allCoords = Array.from(pointers.values()); const allCoords = Array.from(pointers.values());
return { return pointFrom(
x: sum(allCoords, (coords) => coords.x) / allCoords.length, sum(allCoords, ([coordsX, _]) => coordsX) / allCoords.length,
y: sum(allCoords, (coords) => coords.y) / allCoords.length, sum(allCoords, ([_, coordsY]) => coordsY) / allCoords.length,
}; );
}; };
export const getDistance = ([a, b]: readonly PointerCoords[]) => export const isTwoPointerCoords = <Point extends GenericPoint>(
Math.hypot(a.x - b.x, a.y - b.y); arr: Point[],
): arr is [Point, Point] => {
return arr.length === 2;
};
export const getDistance = <Point extends GenericPoint>([
[x1, y1],
[x2, y2],
]: readonly [Point, Point]) => Math.hypot(x1 - x2, y1 - y2);
const sum = <T>(array: readonly T[], mapper: (item: T) => number): number => const sum = <T>(array: readonly T[], mapper: (item: T) => number): number =>
array.reduce((acc, item) => acc + mapper(item), 0); array.reduce((acc, item) => acc + mapper(item), 0);

View file

@ -3,6 +3,7 @@ import {
sceneCoordsToViewportCoords, sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords, viewportCoordsToSceneCoords,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math";
import { getClosestElementBounds } from "@excalidraw/element/bounds"; import { getClosestElementBounds } from "@excalidraw/element/bounds";
@ -14,11 +15,11 @@ import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => { const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
const [x1, y1, x2, y2] = cords; const [x1, y1, x2, y2] = cords;
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords( const [viewportX1, viewportY1] = sceneCoordsToViewportCoords(
{ sceneX: x1, sceneY: y1 }, { sceneX: x1, sceneY: y1 },
appState, appState,
); );
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords( const [viewportX2, viewportY2] = sceneCoordsToViewportCoords(
{ sceneX: x2, sceneY: y2 }, { sceneX: x2, sceneY: y2 },
appState, appState,
); );
@ -39,15 +40,16 @@ export const centerScrollOn = ({
zoom: Zoom; zoom: Zoom;
offsets?: Offsets; offsets?: Offsets;
}) => { }) => {
const [scenePointX, scenePointY] = scenePoint;
let scrollX = let scrollX =
(viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value - (viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value -
scenePoint.x; scenePointX;
scrollX += (offsets?.left ?? 0) / 2 / zoom.value; scrollX += (offsets?.left ?? 0) / 2 / zoom.value;
let scrollY = let scrollY =
(viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value - (viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value -
scenePoint.y; scenePointY;
scrollY += (offsets?.top ?? 0) / 2 / zoom.value; scrollY += (offsets?.top ?? 0) / 2 / zoom.value;
@ -85,7 +87,7 @@ export const calculateScrollCenter = (
const centerY = (y1 + y2) / 2; const centerY = (y1 + y2) / 2;
return centerScrollOn({ return centerScrollOn({
scenePoint: { x: centerX, y: centerY }, scenePoint: pointFrom(centerX, centerY),
viewportDimensions: { width: appState.width, height: appState.height }, viewportDimensions: { width: appState.width, height: appState.height },
zoom: appState.zoom, zoom: appState.zoom,
}); });

View file

@ -7,6 +7,7 @@ import type {
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { MakeBrand } from "@excalidraw/common/utility-types"; import type { MakeBrand } from "@excalidraw/common/utility-types";
import type { GenericPoint } from "@excalidraw/math";
import type { import type {
AppClassProperties, AppClassProperties,
@ -61,7 +62,7 @@ export type InteractiveCanvasRenderConfig = {
// collab-related state // collab-related state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
remoteSelectedElementIds: Map<ExcalidrawElement["id"], SocketId[]>; remoteSelectedElementIds: Map<ExcalidrawElement["id"], SocketId[]>;
remotePointerViewportCoords: Map<SocketId, { x: number; y: number }>; remotePointerViewportCoords: Map<SocketId, GenericPoint>;
remotePointerUserStates: Map<SocketId, UserIdleState>; remotePointerUserStates: Map<SocketId, UserIdleState>;
remotePointerUsernames: Map<SocketId, string>; remotePointerUsernames: Map<SocketId, string>;
remotePointerButton: Map<SocketId, string | undefined>; remotePointerButton: Map<SocketId, string | undefined>;

View file

@ -1327,7 +1327,7 @@ export const getSnapLinesAtPointer = (
) => { ) => {
if (!isSnappingEnabled({ event, selectedElements: [], app })) { if (!isSnappingEnabled({ event, selectedElements: [], app })) {
return { return {
originOffset: { x: 0, y: 0 }, originOffset: pointFrom(0, 0),
snapLines: [], snapLines: [],
}; };
} }
@ -1388,16 +1388,14 @@ export const getSnapLinesAtPointer = (
} }
return { return {
originOffset: { originOffset: pointFrom(
x: verticalSnapLines.length > 0
verticalSnapLines.length > 0 ? verticalSnapLines[0].points[0][0] - pointer.x
? verticalSnapLines[0].points[0][0] - pointer.x : 0,
: 0, horizontalSnapLines.length > 0
y: ? horizontalSnapLines[0].points[0][1] - pointer.y
horizontalSnapLines.length > 0 : 0,
? horizontalSnapLines[0].points[0][1] - pointer.y ),
: 0,
},
snapLines: [...verticalSnapLines, ...horizontalSnapLines], snapLines: [...verticalSnapLines, ...horizontalSnapLines],
}; };
}; };

View file

@ -42,6 +42,7 @@ import type {
ValueOf, ValueOf,
MakeBrand, MakeBrand,
} from "@excalidraw/common/utility-types"; } from "@excalidraw/common/utility-types";
import type { GenericPoint } from "@excalidraw/math";
import type { Action } from "./actions/types"; import type { Action } from "./actions/types";
import type { Spreadsheet } from "./charts"; import type { Spreadsheet } from "./charts";
@ -413,10 +414,7 @@ export interface AppState {
showHyperlinkPopup: false | "info" | "editor"; showHyperlinkPopup: false | "info" | "editor";
selectedLinearElement: LinearElementEditor | null; selectedLinearElement: LinearElementEditor | null;
snapLines: readonly SnapLine[]; snapLines: readonly SnapLine[];
originSnapOffset: { originSnapOffset: GenericPoint | null;
x: number;
y: number;
} | null;
objectsSnapModeEnabled: boolean; objectsSnapModeEnabled: boolean;
/** the user's socket id & username who is being followed on the canvas */ /** the user's socket id & username who is being followed on the canvas */
userToFollow: UserToFollow | null; userToFollow: UserToFollow | null;
@ -456,14 +454,11 @@ export type Zoom = Readonly<{
value: NormalizedZoomValue; value: NormalizedZoomValue;
}>; }>;
export type PointerCoords = Readonly<{ export type PointerCoords = Readonly<GenericPoint>;
x: number;
y: number;
}>;
export type Gesture = { export type Gesture = {
pointers: Map<number, PointerCoords>; pointers: Map<number, PointerCoords>;
lastCenter: { x: number; y: number } | null; lastCenter: PointerCoords | null;
initialDistance: number | null; initialDistance: number | null;
initialScale: number | null; initialScale: number | null;
}; };
@ -717,13 +712,13 @@ export type AppClassProperties = {
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
// The first position at which pointerDown happened // The first position at which pointerDown happened
origin: Readonly<{ x: number; y: number }>; origin: Readonly<GenericPoint>;
// Same as "origin" but snapped to the grid, if grid is on // Same as "origin" but snapped to the grid, if grid is on
originInGrid: Readonly<{ x: number; y: number }>; originInGrid: Readonly<GenericPoint>;
// Scrollbar checks // Scrollbar checks
scrollbars: ReturnType<typeof isOverScrollBars>; scrollbars: ReturnType<typeof isOverScrollBars>;
// The previous pointer position // The previous pointer position
lastCoords: { x: number; y: number }; lastCoords: GenericPoint;
// map of original elements data // map of original elements data
originalElements: Map<string, NonDeleted<ExcalidrawElement>>; originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
resize: { resize: {
@ -732,11 +727,11 @@ export type PointerDownState = Readonly<{
// This is determined on the initial pointer down event // This is determined on the initial pointer down event
isResizing: boolean; isResizing: boolean;
// This is determined on the initial pointer down event // 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 // This is determined on the initial pointer down event
arrowDirection: "origin" | "end"; arrowDirection: "origin" | "end";
// This is a center point of selected elements determined on the initial pointer down event (for rotation only) // 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: { hit: {
// The element the pointer is "hitting", is determined on the initial // The element the pointer is "hitting", is determined on the initial
@ -757,7 +752,7 @@ export type PointerDownState = Readonly<{
// Might change during the pointer interaction // Might change during the pointer interaction
hasOccurred: boolean; hasOccurred: boolean;
// Might change during the pointer interaction // Might change during the pointer interaction
offset: { x: number; y: number } | null; offset: GenericPoint | null;
}; };
// We need to have these in the state so that we can unsubscribe them // We need to have these in the state so that we can unsubscribe them
eventListeners: { eventListeners: {