Refactoring points

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
pull/8539/merge^2
Mark Tolmacs 6 months ago
parent 8ca4cf3260
commit b4cb314090
No known key found for this signature in database

@ -38,7 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import type { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor";
import { StoreAction } from "../store";
import { clamp, roundToStep } from "../../math";
import { clamp, point, roundToStep } from "../../math";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@ -324,7 +324,7 @@ export const zoomToFitBounds = ({
);
const centerScroll = centerScrollOn({
scenePoint: { x: centerX, y: centerY },
scenePoint: point(centerX, centerY),
viewportDimensions: {
width: appState.width,
height: appState.height,

@ -127,7 +127,7 @@ export const actionFinalize = register({
!isLoop &&
multiPointElement.points.length > 1
) {
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement,
-1,
arrayToMap(elements),
@ -135,7 +135,7 @@ export const actionFinalize = register({
maybeBindLinearElement(
multiPointElement,
appState,
{ x, y },
p,
elementsMap,
elements,
);

@ -98,12 +98,7 @@ import {
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import {
arrayToMap,
getFontFamilyString,
getShortcutKey,
tupleToCoors,
} from "../utils";
import { arrayToMap, getFontFamilyString, getShortcutKey } from "../utils";
import { register } from "./register";
import { StoreAction } from "../store";
import { Fonts, getLineHeight } from "../fonts";
@ -1588,7 +1583,7 @@ export const actionChangeArrowType = register({
const startHoveredElement =
!newElement.startBinding &&
getHoveredElementForBinding(
tupleToCoors(startGlobalPoint),
startGlobalPoint,
elements,
elementsMap,
true,
@ -1596,7 +1591,7 @@ export const actionChangeArrowType = register({
const endHoveredElement =
!newElement.endBinding &&
getHoveredElementForBinding(
tupleToCoors(endGlobalPoint),
endGlobalPoint,
elements,
elementsMap,
true,

@ -5,6 +5,7 @@ import type { AppState } from "./types";
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
import type App from "./components/App";
import { SVG_NS } from "./constants";
import { point } from "../math";
export interface Trail {
start(container: SVGSVGElement): void;
@ -135,14 +136,7 @@ export class AnimatedTrail implements Trail {
private drawTrail(trail: LaserPointer, state: AppState): string {
const stroke = trail
.getStrokeOutline(trail.options.size / state.zoom.value)
.map(([x, y]) => {
const result = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y },
state,
);
return [result.x, result.y];
});
.map((p) => sceneCoordsToViewportCoords(point(p[0], p[1]), state));
return getSvgPathFromStroke(stroke, true);
}

File diff suppressed because it is too large Load Diff

@ -164,8 +164,8 @@ export const EyeDropper: React.FC<{
// init color preview else it would show only after the first mouse move
mouseMoveListener({
clientX: stableProps.app.lastViewportPosition.x,
clientY: stableProps.app.lastViewportPosition.y,
clientX: stableProps.app.lastViewportPosition[0],
clientY: stableProps.app.lastViewportPosition[1],
altKey: false,
});

@ -15,6 +15,7 @@ import type {
} from "../../element/types";
import { isRenderThrottlingEnabled } from "../../reactUtils";
import { renderInteractiveScene } from "../../renderer/interactiveScene";
import { point } from "../../../math";
type InteractiveCanvasProps = {
containerRef: React.RefObject<HTMLDivElement>;
@ -103,10 +104,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
remotePointerViewportCoords.set(
socketId,
sceneCoordsToViewportCoords(
{
sceneX: user.pointer.x,
sceneY: user.pointer.y,
},
point(user.pointer.x, user.pointer.y),
props.appState,
),
);

@ -36,7 +36,8 @@ import { trackEvent } from "../../analytics";
import { useAppProps, useExcalidrawAppState } from "../App";
import { isEmbeddableElement } from "../../element/typeChecks";
import { getLinkHandleFromCoords } from "./helpers";
import { point, type GlobalPoint } from "../../../math";
import type { ViewportPoint } from "../../../math";
import { point } from "../../../math";
const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85;
@ -324,8 +325,8 @@ const getCoordsForPopover = (
elementsMap: ElementsMap,
) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 },
const [viewportX, viewportY] = sceneCoordsToViewportCoords(
point(x1 + element.width / 2, y1),
appState,
);
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
@ -387,15 +388,15 @@ const renderTooltip = (
);
const linkViewportCoords = sceneCoordsToViewportCoords(
{ sceneX: linkX, sceneY: linkY },
point(linkX, linkY),
appState,
);
updateTooltipPosition(
tooltipDiv,
{
left: linkViewportCoords.x,
top: linkViewportCoords.y,
left: linkViewportCoords[0],
top: linkViewportCoords[1],
width: linkWidth,
height: linkHeight,
},
@ -419,25 +420,22 @@ const shouldHideLinkPopup = (
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState,
[clientX, clientY]: GlobalPoint,
viewportCoords: ViewportPoint,
): Boolean => {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX, clientY },
appState,
);
const sceneCoords = viewportCoordsToSceneCoords(viewportCoords, appState);
const threshold = 15 / appState.zoom.value;
// hitbox to prevent hiding when hovered in element bounding box
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
if (hitElementBoundingBox(sceneCoords, element, elementsMap)) {
return false;
}
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
// hit box to prevent hiding when hovered in the vertical area between element and popover
if (
sceneX >= x1 &&
sceneX <= x2 &&
sceneY >= y1 - SPACE_BOTTOM &&
sceneY <= y1
sceneCoords[0] >= x1 &&
sceneCoords[0] <= x2 &&
sceneCoords[1] >= y1 - SPACE_BOTTOM &&
sceneCoords[1] <= y1
) {
return false;
}
@ -449,10 +447,12 @@ const shouldHideLinkPopup = (
);
if (
clientX >= popoverX - threshold &&
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
clientY >= popoverY - threshold &&
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
viewportCoords[0] >= popoverX - threshold &&
viewportCoords[0] <=
popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
viewportCoords[1] >= popoverY - threshold &&
viewportCoords[1] <=
popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
) {
return false;
}

@ -81,7 +81,7 @@ export const isPointHittingLink = (
if (
!isMobile &&
appState.viewModeEnabled &&
hitElementBoundingBox(x, y, element, elementsMap)
hitElementBoundingBox(point(x, y), element, elementsMap)
) {
return true;
}

@ -5,6 +5,7 @@ import { getElementAbsoluteCoords } from ".";
import { useExcalidrawAppState } from "../components/App";
import "./ElementCanvasButtons.scss";
import { point } from "../../math";
const CONTAINER_PADDING = 5;
@ -14,8 +15,8 @@ const getContainerCoords = (
elementsMap: ElementsMap,
) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width, sceneY: y1 },
const [viewportX, viewportY] = sceneCoordsToViewportCoords(
point(x1 + element.width, y1),
appState,
);
const x = viewportX - appState.offsetLeft + 10;

@ -49,7 +49,7 @@ import type { ElementUpdate } from "./mutateElement";
import { mutateElement } from "./mutateElement";
import type Scene from "../scene/Scene";
import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils";
import { arrayToMap } from "../utils";
import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
@ -389,7 +389,7 @@ export const getSuggestedBindingsForArrows = (
export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState,
pointerCoords: { x: number; y: number },
pointerCoords: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[],
): void => {
@ -508,10 +508,7 @@ const unbindLinearElement = (
};
export const getHoveredElementForBinding = (
pointerCoords: {
x: number;
y: number;
},
pointer: GlobalPoint,
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
fullShape?: boolean,
@ -522,7 +519,7 @@ export const getHoveredElementForBinding = (
isBindableElement(element, false) &&
bindingBorderTest(
element,
pointerCoords,
pointer,
elementsMap,
// disable fullshape snapping for frame elements so we
// can bind to frame children
@ -1177,14 +1174,12 @@ const getLinearElementEdgeCoors = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap,
): { x: number; y: number } => {
): GlobalPoint => {
const index = startOrEnd === "start" ? 0 : -1;
return tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
index,
elementsMap,
),
return LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
index,
elementsMap,
);
};
@ -1330,7 +1325,7 @@ const newBoundElements = (
export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number },
[x, y]: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap,
fullShape?: boolean,
): boolean => {

@ -42,35 +42,33 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
};
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
x: number;
y: number;
sceneCoords: Point;
element: ExcalidrawElement;
shape: GeometricShape<Point>;
threshold?: number;
frameNameBound?: FrameNameBounds | null;
};
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
x,
y,
export const hitElementItself = ({
sceneCoords,
element,
shape,
threshold = 10,
frameNameBound = null,
}: HitTestArgs<Point>) => {
}: HitTestArgs<GlobalPoint>) => {
let hit = shouldTestInside(element)
? // Since `inShape` tests STRICTLY againt the insides of a shape
// we would need `onShape` as well to include the "borders"
isPointInShape(point(x, y), shape) ||
isPointOnShape(point(x, y), shape, threshold)
: isPointOnShape(point(x, y), shape, threshold);
isPointInShape(sceneCoords, shape) ||
isPointOnShape(sceneCoords, shape, threshold)
: isPointOnShape(sceneCoords, shape, threshold);
// hit test against a frame's name
if (!hit && frameNameBound) {
hit = isPointInShape(point(x, y), {
hit = isPointInShape(sceneCoords, {
type: "polygon",
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
.data as Polygon<Point>,
.data as Polygon<GlobalPoint>,
});
}
@ -78,8 +76,7 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
};
export const hitElementBoundingBox = (
x: number,
y: number,
scenePointer: GlobalPoint,
element: ExcalidrawElement,
elementsMap: ElementsMap,
tolerance = 0,
@ -89,31 +86,27 @@ export const hitElementBoundingBox = (
y1 -= tolerance;
x2 += tolerance;
y2 += tolerance;
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
return isPointWithinBounds(point(x1, y1), scenePointer, point(x2, y2));
};
export const hitElementBoundingBoxOnly = <
Point extends GlobalPoint | LocalPoint,
>(
hitArgs: HitTestArgs<Point>,
export const hitElementBoundingBoxOnly = (
hitArgs: HitTestArgs<GlobalPoint>,
elementsMap: ElementsMap,
) => {
return (
!hitElementItself(hitArgs) &&
// bound text is considered part of the element (even if it's outside the bounding box)
!hitElementBoundText(
hitArgs.x,
hitArgs.y,
hitArgs.sceneCoords,
getBoundTextShape(hitArgs.element, elementsMap),
) &&
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
hitElementBoundingBox(hitArgs.sceneCoords, hitArgs.element, elementsMap)
);
};
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
x: number,
y: number,
textShape: GeometricShape<Point> | null,
export const hitElementBoundText = (
scenePointer: GlobalPoint,
textShape: GeometricShape<GlobalPoint> | null,
): boolean => {
return !!textShape && isPointInShape(point(x, y), textShape);
return !!textShape && isPointInShape(scenePointer, textShape);
};

@ -4,6 +4,7 @@ import type {
Triangle,
Vector,
Radians,
ViewportPoint,
} from "../../math";
import {
point,
@ -21,7 +22,9 @@ export const HEADING_LEFT = [-1, 0] as Heading;
export const HEADING_UP = [0, -1] as Heading;
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
export const headingForDiamond = <
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(
a: Point,
b: Point,
) => {

@ -20,7 +20,6 @@ import {
} from "./bounds";
import type {
AppState,
PointerCoords,
InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
@ -32,7 +31,7 @@ import {
getHoveredElementForBinding,
isBindingEnabled,
} from "./binding";
import { invariant, toBrandedType, tupleToCoors } from "../utils";
import { invariant, toBrandedType } from "../utils";
import {
isBindingElement,
isElbowArrow,
@ -56,6 +55,8 @@ import {
type GlobalPoint,
type LocalPoint,
pointDistance,
pointSubtract,
pointFromPair,
} from "../../math";
import {
getBezierCurveLength,
@ -83,7 +84,7 @@ export class LinearElementEditor {
/** index */
lastClickedPoint: number;
lastClickedIsEndPoint: boolean;
origin: Readonly<{ x: number; y: number }> | null;
origin: GlobalPoint | null;
segmentMidpoint: {
value: GlobalPoint | null;
index: number | null;
@ -94,7 +95,7 @@ export class LinearElementEditor {
/** whether you're dragging a point */
public readonly isDragging: boolean;
public readonly lastUncommittedPoint: LocalPoint | null;
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
public readonly pointerOffset: GlobalPoint;
public readonly startBindingElement:
| ExcalidrawBindableElement
| null
@ -115,7 +116,7 @@ export class LinearElementEditor {
this.selectedPointsIndices = null;
this.lastUncommittedPoint = null;
this.isDragging = false;
this.pointerOffset = { x: 0, y: 0 };
this.pointerOffset = point(0, 0);
this.startBindingElement = "keep";
this.endBindingElement = "keep";
this.pointerDownState = {
@ -219,11 +220,10 @@ export class LinearElementEditor {
static handlePointDragging(
event: PointerEvent,
app: AppClassProperties,
scenePointerX: number,
scenePointerY: number,
scenePointer: GlobalPoint,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
pointSceneCoords: GlobalPoint[],
) => void,
linearElementEditor: LinearElementEditor,
scene: Scene,
@ -287,7 +287,7 @@ export class LinearElementEditor {
element,
elementsMap,
referencePoint,
point(scenePointerX, scenePointerY),
scenePointer,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
@ -309,8 +309,7 @@ export class LinearElementEditor {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
pointSubtract(scenePointer, linearElementEditor.pointerOffset),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
@ -325,8 +324,10 @@ export class LinearElementEditor {
? LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
pointSubtract(
scenePointer,
linearElementEditor.pointerOffset,
),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
)
: point(
@ -350,17 +351,15 @@ export class LinearElementEditor {
// suggest bindings for first and last point if selected
if (isBindingElement(element, false)) {
const coords: { x: number; y: number }[] = [];
const coords: GlobalPoint[] = [];
const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
),
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
),
);
}
@ -369,12 +368,10 @@ export class LinearElementEditor {
selectedPointsIndices[selectedPointsIndices.length - 1];
if (lastSelectedIndex === element.points.length - 1) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
elementsMap,
),
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
elementsMap,
),
);
}
@ -439,12 +436,10 @@ export class LinearElementEditor {
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
elements,
elementsMap,
@ -481,7 +476,7 @@ export class LinearElementEditor {
? [pointerDownState.lastClickedPoint]
: selectedPointsIndices,
isDragging: false,
pointerOffset: { x: 0, y: 0 },
pointerOffset: point(0, 0),
};
}
@ -556,7 +551,7 @@ export class LinearElementEditor {
static getSegmentMidpointHitCoords = (
linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number },
scenePointer: GlobalPoint,
appState: AppState,
elementsMap: ElementsMap,
): GlobalPoint | null => {
@ -569,8 +564,7 @@ export class LinearElementEditor {
element,
elementsMap,
appState.zoom,
scenePointer.x,
scenePointer.y,
scenePointer,
);
if (clickedPointIndex >= 0) {
return null;
@ -594,7 +588,7 @@ export class LinearElementEditor {
existingSegmentMidpointHitCoords[0],
existingSegmentMidpointHitCoords[1],
),
point(scenePointer.x, scenePointer.y),
scenePointer,
);
if (distance <= threshold) {
return existingSegmentMidpointHitCoords;
@ -607,7 +601,7 @@ export class LinearElementEditor {
if (midPoints[index] !== null) {
const distance = pointDistance(
point(midPoints[index]![0], midPoints[index]![1]),
point(scenePointer.x, scenePointer.y),
scenePointer,
);
if (distance <= threshold) {
return midPoints[index];
@ -705,7 +699,7 @@ export class LinearElementEditor {
event: React.PointerEvent<HTMLElement>,
app: AppClassProperties,
store: Store,
scenePointer: { x: number; y: number },
scenePointer: GlobalPoint,
linearElementEditor: LinearElementEditor,
scene: Scene,
): {
@ -759,8 +753,7 @@ export class LinearElementEditor {
LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointer.x,
scenePointer.y,
scenePointer,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
),
],
@ -774,7 +767,7 @@ export class LinearElementEditor {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
lastClickedIsEndPoint: false,
origin: { x: scenePointer.x, y: scenePointer.y },
origin: scenePointer,
segmentMidpoint: {
value: segmentMidpoint,
index: segmentMidpointIndex,
@ -798,8 +791,7 @@ export class LinearElementEditor {
element,
elementsMap,
appState.zoom,
scenePointer.x,
scenePointer.y,
scenePointer,
);
// if we clicked on a point, set the element as hitElement otherwise
// it would get deselected if the point is outside the hitbox area
@ -828,7 +820,7 @@ export class LinearElementEditor {
const cy = (y1 + y2) / 2;
const targetPoint =
clickedPointIndex > -1 &&
pointRotateRads(
pointRotateRads<GlobalPoint>(
point(
element.x + element.points[clickedPointIndex][0],
element.y + element.points[clickedPointIndex][1],
@ -853,7 +845,7 @@ export class LinearElementEditor {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1,
origin: { x: scenePointer.x, y: scenePointer.y },
origin: scenePointer,
segmentMidpoint: {
value: segmentMidpoint,
index: segmentMidpointIndex,
@ -862,11 +854,8 @@ export class LinearElementEditor {
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
? {
x: scenePointer.x - targetPoint[0],
y: scenePointer.y - targetPoint[1],
}
: { x: 0, y: 0 },
? pointSubtract(scenePointer, targetPoint)
: point(0, 0),
};
return ret;
@ -887,8 +876,7 @@ export class LinearElementEditor {
static handlePointerMove(
event: React.PointerEvent<HTMLCanvasElement>,
scenePointerX: number,
scenePointerY: number,
scenePointer: GlobalPoint,
app: AppClassProperties,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): LinearElementEditor | null {
@ -928,7 +916,7 @@ export class LinearElementEditor {
element,
elementsMap,
lastCommittedPoint,
point(scenePointerX, scenePointerY),
scenePointer,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
@ -940,8 +928,10 @@ export class LinearElementEditor {
newPoint = LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y,
pointSubtract(
scenePointer,
appState.editingLinearElement.pointerOffset,
),
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
? null
: app.getEffectiveGridSize(),
@ -1057,8 +1047,7 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
zoom: AppState["zoom"],
x: number,
y: number,
p: GlobalPoint,
) {
const pointHandles = LinearElementEditor.getPointsGlobalCoordinates(
element,
@ -1069,9 +1058,9 @@ export class LinearElementEditor {
// points on the left, thus should take precedence when clicking, if they
// overlap
while (--idx > -1) {
const p = pointHandles[idx];
const handles = pointHandles[idx];
if (
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
pointDistance(p, pointFromPair(handles)) * zoom.value <
// +1px to account for outline stroke
LinearElementEditor.POINT_HANDLE_SIZE + 1
) {
@ -1084,11 +1073,14 @@ export class LinearElementEditor {
static createPointAt(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scenePointerX: number,
scenePointerY: number,
scenePointer: GlobalPoint,
gridSize: NullableGridSize,
): LocalPoint {
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
const pointerOnGrid = getGridPoint(
scenePointer[0],
scenePointer[1],
gridSize,
);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
@ -1337,7 +1329,7 @@ export class LinearElementEditor {
static shouldAddMidpoint(
linearElementEditor: LinearElementEditor,
pointerCoords: PointerCoords,
pointerCoords: GlobalPoint,
appState: AppState,
elementsMap: ElementsMap,
) {
@ -1367,10 +1359,7 @@ export class LinearElementEditor {
}
const origin = linearElementEditor.pointerDownState.origin!;
const dist = pointDistance(
point(origin.x, origin.y),
point(pointerCoords.x, pointerCoords.y),
);
const dist = pointDistance(origin, pointerCoords);
if (
!appState.editingLinearElement &&
dist < DRAGGING_THRESHOLD / appState.zoom.value
@ -1382,7 +1371,7 @@ export class LinearElementEditor {
static addMidpoint(
linearElementEditor: LinearElementEditor,
pointerCoords: PointerCoords,
pointerCoords: GlobalPoint,
app: AppClassProperties,
snapToGrid: boolean,
elementsMap: ElementsMap,
@ -1406,8 +1395,7 @@ export class LinearElementEditor {
const midpoint = LinearElementEditor.createPointAt(
element,
elementsMap,
pointerCoords.x,
pointerCoords.y,
pointerCoords,
snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null,
);
const points = [

@ -1089,8 +1089,7 @@ export const getResizeOffsetXY = (
transformHandleType: MaybeTransformHandleType,
selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
x: number,
y: number,
[x, y]: GlobalPoint,
): [number, number] => {
const [x1, y1, x2, y2] =
selectedElements.length === 1

@ -92,7 +92,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
if (!(isLinearElement(element) && element.points.length <= 2)) {
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders(
point(x1 - SPACING, y1 - SPACING),
point<Point>(x1 - SPACING, y1 - SPACING),
point(x2 + SPACING, y2 + SPACING),
point(cx, cy),
element.angle,
@ -101,7 +101,11 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
for (const [dir, side] of Object.entries(sides)) {
// test to see if x, y are on the line segment
if (
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
pointOnLineSegment(
point<Point>(x, y),
side as LineSegment<Point>,
SPACING,
)
) {
return dir as TransformHandleType;
}
@ -115,8 +119,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
export const getElementWithTransformHandleType = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
scenePointerX: number,
scenePointerY: number,
scenePointer: GlobalPoint,
zoom: Zoom,
pointerType: PointerType,
elementsMap: ElementsMap,
@ -130,8 +133,8 @@ export const getElementWithTransformHandleType = (
element,
elementsMap,
appState,
scenePointerX,
scenePointerY,
scenePointer[0],
scenePointer[1],
zoom,
pointerType,
device,
@ -140,12 +143,9 @@ export const getElementWithTransformHandleType = (
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
};
export const getTransformHandleTypeFromCoords = <
Point extends GlobalPoint | LocalPoint,
>(
export const getTransformHandleTypeFromCoords = (
[x1, y1, x2, y2]: Bounds,
scenePointerX: number,
scenePointerY: number,
scenePointer: GlobalPoint,
zoom: Zoom,
pointerType: PointerType,
device: Device,
@ -163,7 +163,7 @@ export const getTransformHandleTypeFromCoords = <
transformHandles[key as Exclude<TransformHandleType, "rotation">]!;
return (
transformHandle &&
isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY)
isInsideTransformHandle(transformHandle, scenePointer[0], scenePointer[1])
);
});
@ -178,7 +178,7 @@ export const getTransformHandleTypeFromCoords = <
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders(
point(x1 - SPACING, y1 - SPACING),
point<GlobalPoint>(x1 - SPACING, y1 - SPACING),
point(x2 + SPACING, y2 + SPACING),
point(cx, cy),
0 as Radians,
@ -188,8 +188,8 @@ export const getTransformHandleTypeFromCoords = <
// test to see if x, y are on the line segment
if (
pointOnLineSegment(
point(scenePointerX, scenePointerY),
side as LineSegment<Point>,
scenePointer,
side as LineSegment<GlobalPoint>,
SPACING,
)
) {

@ -14,7 +14,7 @@ import {
import BinaryHeap from "../binaryheap";
import { getSizeFromPoints } from "../points";
import { aabbForElement, pointInsideBounds } from "../shapes";
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
import { isAnyTrue, toBrandedType } from "../utils";
import {
bindPointToSnapToElementOutline,
distanceToBindableElement,
@ -1081,13 +1081,13 @@ const getHoveredElements = (
const elements = Array.from(elementsMap.values());
return [
getHoveredElementForBinding(
tupleToCoors(origStartGlobalPoint),
origStartGlobalPoint,
elements,
nonDeletedSceneElementsMap,
true,
),
getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint),
origEndGlobalPoint,
elements,
nonDeletedSceneElementsMap,
true,

@ -5,6 +5,7 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
import type { AppState, Offsets, Zoom } from "../types";
import { getCommonBounds, getElementBounds } from "./bounds";
import { viewportCoordsToSceneCoords } from "../utils";
import { point } from "../../math";
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
@ -33,25 +34,22 @@ export const isElementInViewport = (
) => {
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft,
clientY: viewTransformations.offsetTop,
},
point(viewTransformations.offsetLeft, viewTransformations.offsetTop),
viewTransformations,
);
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft + width,
clientY: viewTransformations.offsetTop + height,
},
point(
viewTransformations.offsetLeft + width,
viewTransformations.offsetTop + height,
),
viewTransformations,
);
return (
topLeftSceneCoords.x <= x2 &&
topLeftSceneCoords.y <= y2 &&
bottomRightSceneCoords.x >= x1 &&
bottomRightSceneCoords.y >= y1
topLeftSceneCoords[0] <= x2 &&
topLeftSceneCoords[1] <= y2 &&
bottomRightSceneCoords[0] >= x1 &&
bottomRightSceneCoords[1] >= y1
);
};
@ -71,25 +69,25 @@ export const isElementCompletelyInViewport = (
) => {
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft + (padding?.left || 0),
clientY: viewTransformations.offsetTop + (padding?.top || 0),
},
point(
viewTransformations.offsetLeft + (padding?.left || 0),
viewTransformations.offsetTop + (padding?.top || 0),
),
viewTransformations,
);
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft + width - (padding?.right || 0),
clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0),
},
point(
viewTransformations.offsetLeft + width - (padding?.right || 0),
viewTransformations.offsetTop + height - (padding?.bottom || 0),
),
viewTransformations,
);
return (
x1 >= topLeftSceneCoords.x &&
y1 >= topLeftSceneCoords.y &&
x2 <= bottomRightSceneCoords.x &&
y2 <= bottomRightSceneCoords.y
x1 >= topLeftSceneCoords[0] &&
y1 >= topLeftSceneCoords[1] &&
x2 <= bottomRightSceneCoords[0] &&
y2 <= bottomRightSceneCoords[1]
);
};

@ -29,6 +29,8 @@ import {
updateOriginalContainerCache,
} from "./containerCache";
import type { ExtractSetType } from "../utility-types";
import type { GlobalPoint } from "../../math";
import { point } from "../../math";
export const normalizeText = (text: string) => {
return (
@ -674,12 +676,12 @@ export const getContainerCenter = (
container: ExcalidrawElement,
appState: AppState,
elementsMap: ElementsMap,
) => {
): GlobalPoint => {
if (!isArrowElement(container)) {
return {
x: container.x + container.width / 2,
y: container.y + container.height / 2,
};
return point(
container.x + container.width / 2,
container.y + container.height / 2,
);
}
const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
@ -692,7 +694,7 @@ export const getContainerCenter = (
container.points[index],
elementsMap,
);
return { x: midPoint[0], y: midPoint[1] };
return point(midPoint[0], midPoint[1]);
}
const index = container.points.length / 2 - 1;
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
@ -709,7 +711,7 @@ export const getContainerCenter = (
elementsMap,
);
}
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
return point(midSegmentMidpoint[0], midSegmentMidpoint[1]);
};
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {

@ -29,6 +29,7 @@ import { getElementLineSegments } from "./element/bounds";
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import type { ReadonlySetLike } from "./utility-types";
import type { GlobalPoint } from "../math";
import { isPointWithinBounds, point } from "../math";
// --------------------------- Frame State ------------------------------------
@ -149,20 +150,13 @@ export const elementOverlapsWithFrame = (
};
export const isCursorInFrame = (
cursorCoords: {
x: number;
y: number;
},
cursorCoords: GlobalPoint,
frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap,
) => {
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
return isPointWithinBounds(
point(fx1, fy1),
point(cursorCoords.x, cursorCoords.y),
point(fx2, fy2),
);
return isPointWithinBounds(point(fx1, fy1), cursorCoords, point(fx2, fy2));
};
export const groupsAreAtLeastIntersectingTheFrame = (

@ -1,15 +0,0 @@
import type { PointerCoords } from "./types";
export const getCenter = (pointers: Map<number, PointerCoords>) => {
const allCoords = Array.from(pointers.values());
return {
x: sum(allCoords, (coords) => coords.x) / allCoords.length,
y: sum(allCoords, (coords) => coords.y) / allCoords.length,
};
};
export const getDistance = ([a, b]: readonly PointerCoords[]) =>
Math.hypot(a.x - b.x, a.y - b.y);
const sum = <T>(array: readonly T[], mapper: (item: T) => number): number =>
array.reduce((acc, item) => acc + mapper(item), 0);

@ -26,7 +26,7 @@ import type {
RenderableElementsMap,
InteractiveCanvasRenderConfig,
} from "../scene/types";
import { distance, getFontString, isRTL } from "../utils";
import { getFontString, isRTL } from "../utils";
import rough from "roughjs/bin/rough";
import type {
AppState,
@ -59,7 +59,7 @@ import { LinearElementEditor } from "../element/linearElementEditor";
import { getContainingFrame } from "../frame";
import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "../../math";
import { isRightAngleRads, rangeExtent, rangeInclusive } from "../../math";
import { getCornerRadius } from "../shapes";
// using a stronger invert (100% vs our regular 93%) and saturate
@ -163,11 +163,11 @@ const cappedElementCanvasSize = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const elementWidth =
isLinearElement(element) || isFreeDrawElement(element)
? distance(x1, x2)
? rangeExtent(rangeInclusive(x1, x2))
: element.width;
const elementHeight =
isLinearElement(element) || isFreeDrawElement(element)
? distance(y1, y2)
? rangeExtent(rangeInclusive(y1, y2))
: element.height;
let width = elementWidth * window.devicePixelRatio + padding * 2;
@ -226,12 +226,16 @@ const generateElementCanvas = (
canvasOffsetX =
element.x > x1
? distance(element.x, x1) * window.devicePixelRatio * scale
? rangeExtent(rangeInclusive(element.x, x1)) *
window.devicePixelRatio *
scale
: 0;
canvasOffsetY =
element.y > y1
? distance(element.y, y1) * window.devicePixelRatio * scale
? rangeExtent(rangeInclusive(element.y, y1)) *
window.devicePixelRatio *
scale
: 0;
context.translate(canvasOffsetX, canvasOffsetY);
@ -263,7 +267,10 @@ const generateElementCanvas = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
const maxDim = Math.max(
rangeExtent(rangeInclusive(x1, x2)),
rangeExtent(rangeInclusive(y1, y2)),
);
boundTextCanvas.width =
maxDim * window.devicePixelRatio * scale + padding * scale * 10;
boundTextCanvas.height =
@ -813,7 +820,10 @@ export const renderElement = (
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
const maxDim = Math.max(
rangeExtent(rangeInclusive(x1, x2)),
rangeExtent(rangeInclusive(y1, y2)),
);
const padding = getCanvasPadding(element);
tempCanvas.width =
maxDim * appState.exportScale + padding * 10 * appState.exportScale;

@ -1,3 +1,4 @@
import type { ViewportPoint } from "../../math";
import { point, type GlobalPoint, type LocalPoint } from "../../math";
import { THEME } from "../constants";
import type { PointSnapLine, PointerSnapLine } from "../snapping";
@ -107,7 +108,7 @@ const drawCross = <Point extends LocalPoint | GlobalPoint>(
context.restore();
};
const drawLine = <Point extends LocalPoint | GlobalPoint>(
const drawLine = <Point extends LocalPoint | GlobalPoint | ViewportPoint>(
from: Point,
to: Point,
context: CanvasRenderingContext2D,
@ -118,7 +119,7 @@ const drawLine = <Point extends LocalPoint | GlobalPoint>(
context.stroke();
};
const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
const drawGapLine = <Point extends LocalPoint | GlobalPoint | ViewportPoint>(
from: Point,
to: Point,
direction: "horizontal" | "vertical",

@ -9,7 +9,7 @@ import type {
import type { Bounds } from "../element/bounds";
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
import { renderSceneToSvg } from "../renderer/staticSvgScene";
import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
import { arrayToMap, getFontString, toBrandedType } from "../utils";
import type { AppState, BinaryFiles } from "../types";
import {
DEFAULT_EXPORT_PADDING,
@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
import { renderStaticScene } from "../renderer/staticScene";
import { Fonts } from "../fonts";
import type { Font } from "../fonts/ExcalidrawFont";
import { rangeExtent, rangeInclusive } from "../../math";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@ -427,8 +428,8 @@ const getCanvasSize = (
exportPadding: number,
): Bounds => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding * 2;
const width = rangeExtent(rangeInclusive(minX, maxX)) + exportPadding * 2;
const height = rangeExtent(rangeInclusive(minY, maxY)) + exportPadding * 2;
return [minX, minY, width, height];
};

@ -1,4 +1,4 @@
import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
import type { AppState, Offsets, Zoom } from "../types";
import type { ExcalidrawElement } from "../element/types";
import {
getCommonBounds,
@ -8,17 +8,19 @@ import {
import {
sceneCoordsToViewportCoords,
tupleToCoors,
viewportCoordsToSceneCoords,
} from "../utils";
import { point, type GlobalPoint } from "../../math";
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
const [x1, y1, x2, y2] = cords;
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
{ sceneX: x1, sceneY: y1 },
const [viewportX1, viewportY1] = sceneCoordsToViewportCoords(
point(x1, y1),
appState,
);
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
{ sceneX: x2, sceneY: y2 },
const [viewportX2, viewportY2] = sceneCoordsToViewportCoords(
point(x2, y2),
appState,
);
return (
@ -33,20 +35,20 @@ export const centerScrollOn = ({
zoom,
offsets,
}: {
scenePoint: PointerCoords;
scenePoint: GlobalPoint;
viewportDimensions: { height: number; width: number };
zoom: Zoom;
offsets?: Offsets;
}) => {
let scrollX =
(viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value -
scenePoint.x;
scenePoint[0];
scrollX += (offsets?.left ?? 0) / 2 / zoom.value;
let scrollY =
(viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value -
scenePoint.y;
scenePoint[1];
scrollY += (offsets?.top ?? 0) / 2 / zoom.value;
@ -73,9 +75,11 @@ export const calculateScrollCenter = (
if (isOutsideViewPort(appState, [x1, y1, x2, y2])) {
[x1, y1, x2, y2] = getClosestElementBounds(
elements,
viewportCoordsToSceneCoords(
{ clientX: appState.scrollX, clientY: appState.scrollY },
appState,
tupleToCoors(
viewportCoordsToSceneCoords(
point(appState.scrollX, appState.scrollY),
appState,
),
),
);
}
@ -84,7 +88,7 @@ export const calculateScrollCenter = (
const centerY = (y1 + y2) / 2;
return centerScrollOn({
scenePoint: { x: centerX, y: centerY },
scenePoint: point(centerX, centerY),
viewportDimensions: { width: appState.width, height: appState.height },
zoom: appState.zoom,
});

@ -19,6 +19,7 @@ import type {
PendingExcalidrawElements,
} from "../types";
import type { MakeBrand } from "../utility-types";
import type { ViewportPoint } from "../../math";
export type RenderableElementsMap = NonDeletedElementsMap &
MakeBrand<"RenderableElementsMap">;
@ -52,7 +53,7 @@ export type InteractiveCanvasRenderConfig = {
// collab-related state
// ---------------------------------------------------------------------------
remoteSelectedElementIds: Map<ExcalidrawElement["id"], SocketId[]>;
remotePointerViewportCoords: Map<SocketId, { x: number; y: number }>;
remotePointerViewportCoords: Map<SocketId, ViewportPoint>;
remotePointerUserStates: Map<SocketId, UserIdleState>;
remotePointerUsernames: Map<SocketId, string>;
remotePointerButton: Map<SocketId, string | undefined>;

@ -1,3 +1,4 @@
import type { ViewportPoint } from "../math";
import {
isPoint,
point,
@ -435,7 +436,9 @@ export const aabbForElement = (
return bounds;
};
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
export const pointInsideBounds = <
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(
p: P,
bounds: Bounds,
): boolean =>

@ -41,6 +41,7 @@ import type { ContextMenuItems } from "./components/ContextMenu";
import type { SnapLine } from "./snapping";
import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types";
import type { StoreActionType } from "./store";
import type { GlobalPoint } from "../math";
export type SocketId = string & { _brand: "SocketId" };
@ -415,14 +416,9 @@ export type Zoom = Readonly<{
value: NormalizedZoomValue;
}>;
export type PointerCoords = Readonly<{
x: number;
y: number;
}>;
export type Gesture = {
pointers: Map<number, PointerCoords>;
lastCenter: { x: number; y: number } | null;
pointers: Map<number, GlobalPoint>;
lastCenter: GlobalPoint | null;
initialDistance: number | null;
initialScale: number | null;
};
@ -661,17 +657,17 @@ export type AppClassProperties = {
excalidrawContainerValue: App["excalidrawContainerValue"];
};
export type PointerDownState = Readonly<{
export type PointerDownState = {
// The first position at which pointerDown happened
origin: Readonly<{ x: number; y: number }>;
origin: Readonly<GlobalPoint>;
// Same as "origin" but snapped to the grid, if grid is on
originInGrid: Readonly<{ x: number; y: number }>;
// Scrollbar checks
scrollbars: ReturnType<typeof isOverScrollBars>;
scrollbars: Readonly<ReturnType<typeof isOverScrollBars>>;
// The previous pointer position
lastCoords: { x: number; y: number };
lastCoords: GlobalPoint;
// map of original elements data
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
originalElements: Readonly<Map<string, NonDeleted<ExcalidrawElement>>>;
resize: {
// Handle when resizing, might change during the pointer interaction
handleType: MaybeTransformHandleType;
@ -698,12 +694,12 @@ export type PointerDownState = Readonly<{
hasBeenDuplicated: boolean;
hasHitCommonBoundingBoxOfSelectedElements: boolean;
};
withCmdOrCtrl: boolean;
withCmdOrCtrl: Readonly<boolean>;
drag: {
// Might change during the pointer interaction
hasOccurred: boolean;
// Might change during the pointer interaction
offset: { x: number; y: number } | null;
offset: Readonly<{ x: number; y: number }> | null;
};
// We need to have these in the state so that we can unsubscribe them
eventListeners: {
@ -719,7 +715,7 @@ export type PointerDownState = Readonly<{
boxSelection: {
hasOccurred: boolean;
};
}>;
};
export type UnsubscribeCallback = () => void;

@ -1,4 +1,5 @@
import { average } from "../math";
import type { GlobalPoint, ViewportPoint } from "../math";
import { average, point } from "../math";
import { COLOR_PALETTE } from "./colors";
import type { EVENT } from "./constants";
import {
@ -363,8 +364,6 @@ export const removeSelection = () => {
}
};
export const distance = (x: number, y: number) => Math.abs(x - y);
export const updateActiveTool = (
appState: Pick<AppState, "activeTool">,
data: ((
@ -419,7 +418,7 @@ export const getShortcutKey = (shortcut: string): string => {
};
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
[clientX, clientY]: ViewportPoint,
{
zoom,
offsetLeft,
@ -433,15 +432,15 @@ export const viewportCoordsToSceneCoords = (
scrollX: number;
scrollY: number;
},
) => {
): GlobalPoint => {
const x = (clientX - offsetLeft) / zoom.value - scrollX;
const y = (clientY - offsetTop) / zoom.value - scrollY;
return { x, y };
return point<GlobalPoint>(x, y);
};
export const sceneCoordsToViewportCoords = (
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
[sceneX, sceneY]: GlobalPoint,
{
zoom,
offsetLeft,
@ -455,10 +454,10 @@ export const sceneCoordsToViewportCoords = (
scrollX: number;
scrollY: number;
},
) => {
): ViewportPoint => {
const x = (sceneX + scrollX) * zoom.value + offsetLeft;
const y = (sceneY + scrollY) * zoom.value + offsetTop;
return { x, y };
return point(x, y);
};
export const getGlobalCSSVariable = (name: string) =>

@ -4,6 +4,7 @@ import type {
LocalPoint,
PolarCoords,
Radians,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils";
@ -23,10 +24,9 @@ export const normalizeRadians = (angle: Radians): Radians => {
* (x, y) for the center point 0,0 where the first number returned is the radius,
* the second is the angle in radians.
*/
export const cartesian2Polar = <P extends GlobalPoint | LocalPoint>([
x,
y,
]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
export const cartesian2Polar = <
P extends GlobalPoint | LocalPoint | ViewportPoint,
>([x, y]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
export function degreesToRadians(degrees: Degrees): Radians {
return ((degrees * Math.PI) / 180) as Radians;

@ -1,12 +1,19 @@
import { cartesian2Polar } from "./angle";
import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types";
import type {
GlobalPoint,
LocalPoint,
SymmetricArc,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils";
/**
* Determines if a cartesian point lies on a symmetric arc, i.e. an arc which
* is part of a circle contour centered on 0, 0.
*/
export const isPointOnSymmetricArc = <P extends GlobalPoint | LocalPoint>(
export const isPointOnSymmetricArc = <
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
point: P,
): boolean => {

@ -5,6 +5,7 @@ import type {
Radians,
Degrees,
Vector,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils";
import { vectorFromPoint, vectorScale } from "./vector";
@ -16,7 +17,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
* @param y The Y coordinate
* @returns The branded and created point
*/
export function point<Point extends GlobalPoint | LocalPoint>(
export function point<Point extends GlobalPoint | LocalPoint | ViewportPoint>(
x: number,
y: number,
): Point {
@ -29,9 +30,9 @@ export function point<Point extends GlobalPoint | LocalPoint>(
* @param numberArray The number array to check and to convert to Point
* @returns The point instance
*/
export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
numberArray: number[],
): Point | undefined {
export function pointFromArray<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(numberArray: number[]): Point | undefined {
return numberArray.length === 2
? point<Point>(numberArray[0], numberArray[1])
: undefined;
@ -43,9 +44,9 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
* @param pair A number pair to convert to Point
* @returns The point instance
*/
export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
pair: [number, number],
): Point {
export function pointFromPair<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(pair: [number, number]): Point {
return pair as Point;
}
@ -55,9 +56,9 @@ export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
* @param v The vector to convert
* @returns The point the vector points at with origin 0,0
*/
export function pointFromVector<P extends GlobalPoint | LocalPoint>(
v: Vector,
): P {
export function pointFromVector<
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(v: Vector): P {
return v as unknown as P;
}
@ -67,7 +68,9 @@ export function pointFromVector<P extends GlobalPoint | LocalPoint>(
* @param p The value to attempt verification on
* @returns TRUE if the provided value has the shape of a local or global point
*/
export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
export function isPoint(
p: unknown,
): p is LocalPoint | GlobalPoint | ViewportPoint {
return (
Array.isArray(p) &&
p.length === 2 &&
@ -86,10 +89,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
* @param b Point The second point to compare
* @returns TRUE if the points are sufficiently close to each other
*/
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
a: Point,
b: Point,
): boolean {
export function pointsEqual<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(a: Point, b: Point): boolean {
const abs = Math.abs;
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
}
@ -102,11 +104,9 @@ export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
* @param angle The radians to rotate the point by
* @returns The rotated point
*/
export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
[x, y]: Point,
[cx, cy]: Point,
angle: Radians,
): Point {
export function pointRotateRads<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>([x, y]: Point, [cx, cy]: Point, angle: Radians): Point {
return point(
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
@ -121,11 +121,9 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
* @param angle The degree to rotate the point by
* @returns The rotated point
*/
export function pointRotateDegs<Point extends GlobalPoint | LocalPoint>(
point: Point,
center: Point,
angle: Degrees,
): Point {
export function pointRotateDegs<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(point: Point, center: Point, angle: Degrees): Point {
return pointRotateRads(point, center, degreesToRadians(angle));
}
@ -143,8 +141,8 @@ export function pointRotateDegs<Point extends GlobalPoint | LocalPoint>(
*/
// TODO 99% of use is translating between global and local coords, which need to be formalized
export function pointTranslate<
From extends GlobalPoint | LocalPoint,
To extends GlobalPoint | LocalPoint,
From extends GlobalPoint | LocalPoint | ViewportPoint,
To extends GlobalPoint | LocalPoint | ViewportPoint,
>(p: From, v: Vector = [0, 0] as Vector): To {
return point(p[0] + v[0], p[1] + v[1]);
}
@ -156,8 +154,14 @@ export function pointTranslate<
* @param b The other point to create the middle point for
* @returns The middle point
*/
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
export function pointCenter<P extends LocalPoint | GlobalPoint | ViewportPoint>(
...p: P[]
): P {
return pointFromPair(
p
.reduce((mid, x) => [mid[0] + x[0], mid[1] + x[1]], [0, 0])
.map((x) => x / p.length) as [number, number],
);
}
/**
@ -168,10 +172,9 @@ export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
* @param b The other point to act like the vector to translate by
* @returns
*/
export function pointAdd<Point extends LocalPoint | GlobalPoint>(
a: Point,
b: Point,
): Point {
export function pointAdd<
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(a: Point, b: Point): Point {
return point(a[0] + b[0], a[1] + b[1]);
}
@ -183,10 +186,9 @@ export function pointAdd<Point extends LocalPoint | GlobalPoint>(
* @param b The point which will act like a vector
* @returns The resulting point
*/
export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
a: Point,
b: Point,
): Point {
export function pointSubtract<
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(a: Point, b: Point): Point {
return point(a[0] - b[0], a[1] - b[1]);
}
@ -197,10 +199,9 @@ export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
* @param b Second point
* @returns The euclidean distance between the two points.
*/
export function pointDistance<P extends LocalPoint | GlobalPoint>(
a: P,
b: P,
): number {
export function pointDistance<
P extends LocalPoint | GlobalPoint | ViewportPoint,
>(a: P, b: P): number {
return Math.hypot(b[0] - a[0], b[1] - a[1]);
}
@ -213,10 +214,9 @@ export function pointDistance<P extends LocalPoint | GlobalPoint>(
* @param b Second point
* @returns The euclidean distance between the two points.
*/
export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
a: P,
b: P,
): number {
export function pointDistanceSq<
P extends LocalPoint | GlobalPoint | ViewportPoint,
>(a: P, b: P): number {
return Math.hypot(b[0] - a[0], b[1] - a[1]);
}
@ -228,7 +228,9 @@ export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
* @param multiplier The scaling factor
* @returns
*/
export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
export const pointScaleFromOrigin = <
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(
p: P,
mid: P,
multiplier: number,
@ -243,7 +245,9 @@ export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
* @param r The other point to compare against
* @returns TRUE if q is indeed between p and r
*/
export const isPointWithinBounds = <P extends GlobalPoint | LocalPoint>(
export const isPointWithinBounds = <
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(
p: P,
q: P,
r: P,

@ -1,6 +1,6 @@
import { pointsEqual } from "./point";
import { lineSegment, pointOnLineSegment } from "./segment";
import type { GlobalPoint, LocalPoint, Polygon } from "./types";
import type { GlobalPoint, LocalPoint, Polygon, ViewportPoint } from "./types";
import { PRECISION } from "./utils";
export function polygon<Point extends GlobalPoint | LocalPoint>(
@ -9,13 +9,15 @@ export function polygon<Point extends GlobalPoint | LocalPoint>(
return polygonClose(points) as Polygon<Point>;
}
export function polygonFromPoints<Point extends GlobalPoint | LocalPoint>(
points: Point[],
) {
export function polygonFromPoints<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(points: Point[]) {
return polygonClose(points) as Polygon<Point>;
}
export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
export const polygonIncludesPoint = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
polygon: Polygon<Point>,
) => {
@ -40,7 +42,9 @@ export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
return inside;
};
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
export const pointOnPolygon = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
p: Point,
poly: Polygon<Point>,
threshold = PRECISION,
@ -57,7 +61,7 @@ export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
return on;
};
function polygonClose<Point extends LocalPoint | GlobalPoint>(
function polygonClose<Point extends LocalPoint | GlobalPoint | ViewportPoint>(
polygon: Point[],
) {
return polygonIsClosed(polygon)
@ -65,8 +69,8 @@ function polygonClose<Point extends LocalPoint | GlobalPoint>(
: ([...polygon, polygon[0]] as Polygon<Point>);
}
function polygonIsClosed<Point extends LocalPoint | GlobalPoint>(
polygon: Point[],
) {
function polygonIsClosed<
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(polygon: Point[]) {
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
}

@ -71,8 +71,8 @@ export const rangeIntersection = (
* Determine if a value is inside a range.
*
* @param value The value to check
* @param range The range
* @returns
* @param range The range to check
* @returns TRUE if the value is in range
*/
export const rangeIncludesValue = (
value: number,
@ -80,3 +80,13 @@ export const rangeIncludesValue = (
): boolean => {
return value >= min && value <= max;
};
/**
* Determine the distance between the start and end of the range.
*
* @param range The range of which to measure the extent of
* @returns The scalar distance or extent of the start and end of the range
*/
export function rangeExtent([a, b]: InclusiveRange) {
return Math.abs(a - b);
}

@ -4,7 +4,13 @@ import {
pointFromVector,
pointRotateRads,
} from "./point";
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
import type {
GlobalPoint,
LineSegment,
LocalPoint,
Radians,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils";
import {
vectorAdd,
@ -20,7 +26,7 @@ import {
* @param points The two points delimiting the line segment on each end
* @returns The line segment delineated by the points
*/
export function lineSegment<P extends GlobalPoint | LocalPoint>(
export function lineSegment<P extends GlobalPoint | LocalPoint | ViewportPoint>(
a: P,
b: P,
): LineSegment<P> {
@ -57,7 +63,9 @@ export const isLineSegment = <Point extends GlobalPoint | LocalPoint>(
* @param origin
* @returns
*/
export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
export const lineSegmentRotate = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
l: LineSegment<Point>,
angle: Radians,
origin?: Point,
@ -72,7 +80,9 @@ export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
* Calculates the point two line segments with a definite start and end point
* intersect at.
*/
export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
export const segmentsIntersectAt = <
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(
a: Readonly<LineSegment<Point>>,
b: Readonly<LineSegment<Point>>,
): Point | null => {
@ -105,7 +115,9 @@ export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
return null;
};
export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
export const pointOnLineSegment = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
line: LineSegment<Point>,
threshold = PRECISION,
@ -119,7 +131,9 @@ export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
return distance < threshold;
};
export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
export const distanceToLineSegment = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
line: LineSegment<Point>,
) => {

@ -43,6 +43,13 @@ export type LocalPoint = [x: number, y: number] & {
_brand: "excalimath__localpoint";
};
/**
* Represents a 2D position on the browser viewport.
*/
export type ViewportPoint = [x: number, y: number] & {
_brand: "excalimath_viewportpoint";
};
// Line
/**
@ -57,7 +64,10 @@ export type Line<P extends GlobalPoint | LocalPoint> = [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<P extends GlobalPoint | LocalPoint> = [a: P, b: P] & {
export type LineSegment<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
a: P,
b: P,
] & {
_brand: "excalimath_linesegment";
};
@ -93,9 +103,10 @@ export type Triangle<P extends GlobalPoint | LocalPoint> = [
* A polygon is a closed shape by connecting the given points
* rectangles and diamonds are modelled by polygons
*/
export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
_brand: "excalimath_polygon";
};
export type Polygon<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
Point[] & {
_brand: "excalimath_polygon";
};
//
// Curve
@ -104,7 +115,7 @@ export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
/**
* Cubic bezier curve with four control points
*/
export type Curve<Point extends GlobalPoint | LocalPoint> = [
export type Curve<Point extends GlobalPoint | LocalPoint | ViewportPoint> = [
Point,
Point,
Point,

@ -1,4 +1,4 @@
import type { GlobalPoint, LocalPoint, Vector } from "./types";
import type { GlobalPoint, LocalPoint, Vector, ViewportPoint } from "./types";
/**
* Create a vector from the x and y coordiante elements.
@ -23,10 +23,9 @@ export function vector(
* @param origin The origin point in a given coordiante system
* @returns The created vector from the point and the origin
*/
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
p: Point,
origin: Point = [0, 0] as Point,
): Vector {
export function vectorFromPoint<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(p: Point, origin: Point = [0, 0] as Point): Vector {
return vector(p[0] - origin[0], p[1] - origin[1]);
}

@ -4,7 +4,7 @@ import {
pointOnEllipse,
type GeometricShape,
} from "./geometry/shape";
import type { Curve } from "../math";
import type { Curve, ViewportPoint } from "../math";
import {
lineSegment,
point,
@ -18,7 +18,9 @@ import {
} from "../math";
// check if the given point is considered on the given shape's border
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
export const isPointOnShape = <
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(
point: Point,
shape: GeometricShape<Point>,
tolerance = 0,
@ -45,21 +47,21 @@ export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
// check if the given point is considered inside the element's border
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
point: Point,
p: Point,
shape: GeometricShape<Point>,
) => {
switch (shape.type) {
case "polygon":
return polygonIncludesPoint(point, shape.data);
return polygonIncludesPoint(p, shape.data);
case "line":
return false;
case "curve":
return false;
case "ellipse":
return pointInEllipse(point, shape.data);
return pointInEllipse(p, shape.data);
case "polyline": {
const polygon = polygonFromPoints(shape.data.flat());
return polygonIncludesPoint(point, polygon);
return polygonIncludesPoint(p, polygon);
}
case "polycurve": {
return false;
@ -77,7 +79,9 @@ export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
return polygonIncludesPoint(point, bounds);
};
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
const pointOnPolycurve = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
polycurve: Polycurve<Point>,
tolerance: number,
@ -85,7 +89,9 @@ const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
};
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
const cubicBezierEquation = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
curve: Curve<Point>,
) => {
const [p0, p1, p2, p3] = curve;
@ -97,7 +103,9 @@ const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
p0[idx] * Math.pow(t, 3);
};
const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
const polyLineFromCurve = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
curve: Curve<Point>,
segments = 10,
): Polyline<Point> => {
@ -119,7 +127,9 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
return lineSegments;
};
export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
export const pointOnCurve = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
curve: Curve<Point>,
threshold: number,
@ -127,7 +137,9 @@ export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
};
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
export const pointOnPolyline = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
polyline: Polyline<Point>,
threshold = 10e-5,

@ -11,18 +11,6 @@ import {
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
describe("point and line", () => {
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
// it("point on left or right of line", () => {
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
// });
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
it("point on the line", () => {

@ -12,7 +12,13 @@
* to pure shapes
*/
import type { Curve, LineSegment, Polygon, Radians } from "../../math";
import type {
Curve,
LineSegment,
Polygon,
Radians,
ViewportPoint,
} from "../../math";
import {
curve,
lineSegment,
@ -56,24 +62,27 @@ import { invariant } from "../../excalidraw/utils";
// a polyline (made up term here) is a line consisting of other line segments
// this corresponds to a straight line element in the editor but it could also
// be used to model other elements
export type Polyline<Point extends GlobalPoint | LocalPoint> =
export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
LineSegment<Point>[];
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
// curve on the canvas
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
Curve<Point>[];
// 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<Point extends GlobalPoint | LocalPoint> = {
export type Ellipse<Point extends GlobalPoint | LocalPoint | ViewportPoint> = {
center: Point;
angle: Radians;
halfWidth: number;
halfHeight: number;
};
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
export type GeometricShape<
Point extends GlobalPoint | LocalPoint | ViewportPoint,
> =
| {
type: "line";
data: LineSegment<Point>;
@ -239,7 +248,9 @@ export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
};
};
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
const polylineFromPoints = <
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(
points: Point[],
): Polyline<Point> => {
let previousPoint: Point = points[0];
@ -254,13 +265,15 @@ const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
return polyline;
};
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
export const getFreedrawShape = <
Point extends GlobalPoint | LocalPoint | ViewportPoint,
>(
element: ExcalidrawFreeDrawElement,
center: Point,
isClosed: boolean = false,
): GeometricShape<Point> => {
const transform = (p: Point) =>
pointRotateRads(
const transform = (p: Point): Point =>
pointRotateRads<Point>(
pointFromVector(
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
),
@ -391,7 +404,9 @@ export const segmentIntersectRectangleElement = <
.filter((i): i is Point => !!i);
};
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
const distanceToEllipse = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
p: Point,
ellipse: Ellipse<Point>,
) => {
@ -445,7 +460,9 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
};
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
export const pointOnEllipse = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
point: Point,
ellipse: Ellipse<Point>,
threshold = PRECISION,
@ -453,7 +470,9 @@ export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
return distanceToEllipse(point, ellipse) <= threshold;
};
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
export const pointInEllipse = <
Point extends LocalPoint | GlobalPoint | ViewportPoint,
>(
p: Point,
ellipse: Ellipse<Point>,
) => {

Loading…
Cancel
Save