Refactoring points

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2024-09-20 21:32:32 +02:00
parent 8ca4cf3260
commit b4cb314090
No known key found for this signature in database
40 changed files with 746 additions and 783 deletions

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -164,8 +164,8 @@ export const EyeDropper: React.FC<{
// 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
mouseMoveListener({ mouseMoveListener({
clientX: stableProps.app.lastViewportPosition.x, clientX: stableProps.app.lastViewportPosition[0],
clientY: stableProps.app.lastViewportPosition.y, clientY: stableProps.app.lastViewportPosition[1],
altKey: false, altKey: false,
}); });

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { getElementAbsoluteCoords } from ".";
import { useExcalidrawAppState } from "../components/App"; import { useExcalidrawAppState } from "../components/App";
import "./ElementCanvasButtons.scss"; import "./ElementCanvasButtons.scss";
import { point } from "../../math";
const CONTAINER_PADDING = 5; const CONTAINER_PADDING = 5;
@ -14,8 +15,8 @@ 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 }, point(x1 + element.width, y1),
appState, appState,
); );
const x = viewportX - appState.offsetLeft + 10; const x = viewportX - appState.offsetLeft + 10;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
import type { AppState, Offsets, Zoom } from "../types"; import type { AppState, Offsets, Zoom } from "../types";
import { getCommonBounds, getElementBounds } from "./bounds"; import { getCommonBounds, getElementBounds } from "./bounds";
import { viewportCoordsToSceneCoords } from "../utils"; 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 // 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' // - 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 [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords( const topLeftSceneCoords = viewportCoordsToSceneCoords(
{ point(viewTransformations.offsetLeft, viewTransformations.offsetTop),
clientX: viewTransformations.offsetLeft,
clientY: viewTransformations.offsetTop,
},
viewTransformations, viewTransformations,
); );
const bottomRightSceneCoords = viewportCoordsToSceneCoords( const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{ point(
clientX: viewTransformations.offsetLeft + width, viewTransformations.offsetLeft + width,
clientY: viewTransformations.offsetTop + height, viewTransformations.offsetTop + height,
}, ),
viewTransformations, viewTransformations,
); );
return ( return (
topLeftSceneCoords.x <= x2 && topLeftSceneCoords[0] <= x2 &&
topLeftSceneCoords.y <= y2 && topLeftSceneCoords[1] <= y2 &&
bottomRightSceneCoords.x >= x1 && bottomRightSceneCoords[0] >= x1 &&
bottomRightSceneCoords.y >= y1 bottomRightSceneCoords[1] >= y1
); );
}; };
@ -71,25 +69,25 @@ export const isElementCompletelyInViewport = (
) => { ) => {
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords( const topLeftSceneCoords = viewportCoordsToSceneCoords(
{ point(
clientX: viewTransformations.offsetLeft + (padding?.left || 0), viewTransformations.offsetLeft + (padding?.left || 0),
clientY: viewTransformations.offsetTop + (padding?.top || 0), viewTransformations.offsetTop + (padding?.top || 0),
}, ),
viewTransformations, viewTransformations,
); );
const bottomRightSceneCoords = viewportCoordsToSceneCoords( const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{ point(
clientX: viewTransformations.offsetLeft + width - (padding?.right || 0), viewTransformations.offsetLeft + width - (padding?.right || 0),
clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0), viewTransformations.offsetTop + height - (padding?.bottom || 0),
}, ),
viewTransformations, viewTransformations,
); );
return ( return (
x1 >= topLeftSceneCoords.x && x1 >= topLeftSceneCoords[0] &&
y1 >= topLeftSceneCoords.y && y1 >= topLeftSceneCoords[1] &&
x2 <= bottomRightSceneCoords.x && x2 <= bottomRightSceneCoords[0] &&
y2 <= bottomRightSceneCoords.y y2 <= bottomRightSceneCoords[1]
); );
}; };

View file

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

View file

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

View file

@ -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);

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import type {
PendingExcalidrawElements, PendingExcalidrawElements,
} from "../types"; } from "../types";
import type { MakeBrand } from "../utility-types"; import type { MakeBrand } from "../utility-types";
import type { ViewportPoint } from "../../math";
export type RenderableElementsMap = NonDeletedElementsMap & export type RenderableElementsMap = NonDeletedElementsMap &
MakeBrand<"RenderableElementsMap">; MakeBrand<"RenderableElementsMap">;
@ -52,7 +53,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, ViewportPoint>;
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

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,19 @@
import { cartesian2Polar } from "./angle"; import { cartesian2Polar } from "./angle";
import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types"; import type {
GlobalPoint,
LocalPoint,
SymmetricArc,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
/** /**
* Determines if a cartesian point lies on a symmetric arc, i.e. an arc which * 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. * 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, { radius: arcRadius, startAngle, endAngle }: SymmetricArc,
point: P, point: P,
): boolean => { ): boolean => {

View file

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

View file

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

View file

@ -71,8 +71,8 @@ export const rangeIntersection = (
* Determine if a value is inside a range. * Determine if a value is inside a range.
* *
* @param value The value to check * @param value The value to check
* @param range The range * @param range The range to check
* @returns * @returns TRUE if the value is in range
*/ */
export const rangeIncludesValue = ( export const rangeIncludesValue = (
value: number, value: number,
@ -80,3 +80,13 @@ export const rangeIncludesValue = (
): boolean => { ): boolean => {
return value >= min && value <= max; 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);
}

View file

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

View file

@ -43,6 +43,13 @@ export type LocalPoint = [x: number, y: number] & {
_brand: "excalimath__localpoint"; _brand: "excalimath__localpoint";
}; };
/**
* Represents a 2D position on the browser viewport.
*/
export type ViewportPoint = [x: number, y: number] & {
_brand: "excalimath_viewportpoint";
};
// Line // 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 * line that is bounded by two distinct end points, and
* contains every point on the line that is between its endpoints. * 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"; _brand: "excalimath_linesegment";
}; };
@ -93,7 +103,8 @@ export type Triangle<P extends GlobalPoint | LocalPoint> = [
* A polygon is a closed shape by connecting the given points * A polygon is a closed shape by connecting the given points
* rectangles and diamonds are modelled by polygons * rectangles and diamonds are modelled by polygons
*/ */
export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & { export type Polygon<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
Point[] & {
_brand: "excalimath_polygon"; _brand: "excalimath_polygon";
}; };
@ -104,7 +115,7 @@ export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
/** /**
* Cubic bezier curve with four control points * 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, Point,
Point, Point,

View file

@ -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. * 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 * @param origin The origin point in a given coordiante system
* @returns The created vector from the point and the origin * @returns The created vector from the point and the origin
*/ */
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>( export function vectorFromPoint<
p: Point, Point extends GlobalPoint | LocalPoint | ViewportPoint,
origin: Point = [0, 0] as Point, >(p: Point, origin: Point = [0, 0] as Point): Vector {
): Vector {
return vector(p[0] - origin[0], p[1] - origin[1]); return vector(p[0] - origin[0], p[1] - origin[1]);
} }

View file

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

View file

@ -11,18 +11,6 @@ import {
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape"; import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
describe("point and line", () => { 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)); const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
it("point on the line", () => { it("point on the line", () => {

View file

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