mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Refactoring points
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
8ca4cf3260
commit
b4cb314090
40 changed files with 746 additions and 783 deletions
|
@ -38,7 +38,7 @@ import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
|||
import type { SceneBounds } from "../element/bounds";
|
||||
import { setCursor } from "../cursor";
|
||||
import { StoreAction } from "../store";
|
||||
import { clamp, roundToStep } from "../../math";
|
||||
import { clamp, point, roundToStep } from "../../math";
|
||||
|
||||
export const actionChangeViewBackgroundColor = register({
|
||||
name: "changeViewBackgroundColor",
|
||||
|
@ -324,7 +324,7 @@ export const zoomToFitBounds = ({
|
|||
);
|
||||
|
||||
const centerScroll = centerScrollOn({
|
||||
scenePoint: { x: centerX, y: centerY },
|
||||
scenePoint: point(centerX, centerY),
|
||||
viewportDimensions: {
|
||||
width: appState.width,
|
||||
height: appState.height,
|
||||
|
|
|
@ -127,7 +127,7 @@ export const actionFinalize = register({
|
|||
!isLoop &&
|
||||
multiPointElement.points.length > 1
|
||||
) {
|
||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
multiPointElement,
|
||||
-1,
|
||||
arrayToMap(elements),
|
||||
|
@ -135,7 +135,7 @@ export const actionFinalize = register({
|
|||
maybeBindLinearElement(
|
||||
multiPointElement,
|
||||
appState,
|
||||
{ x, y },
|
||||
p,
|
||||
elementsMap,
|
||||
elements,
|
||||
);
|
||||
|
|
|
@ -98,12 +98,7 @@ import {
|
|||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import {
|
||||
arrayToMap,
|
||||
getFontFamilyString,
|
||||
getShortcutKey,
|
||||
tupleToCoors,
|
||||
} from "../utils";
|
||||
import { arrayToMap, getFontFamilyString, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
import { StoreAction } from "../store";
|
||||
import { Fonts, getLineHeight } from "../fonts";
|
||||
|
@ -1588,7 +1583,7 @@ export const actionChangeArrowType = register({
|
|||
const startHoveredElement =
|
||||
!newElement.startBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(startGlobalPoint),
|
||||
startGlobalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
true,
|
||||
|
@ -1596,7 +1591,7 @@ export const actionChangeArrowType = register({
|
|||
const endHoveredElement =
|
||||
!newElement.endBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(endGlobalPoint),
|
||||
endGlobalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
true,
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { AppState } from "./types";
|
|||
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
|
||||
import type App from "./components/App";
|
||||
import { SVG_NS } from "./constants";
|
||||
import { point } from "../math";
|
||||
|
||||
export interface Trail {
|
||||
start(container: SVGSVGElement): void;
|
||||
|
@ -135,14 +136,7 @@ export class AnimatedTrail implements Trail {
|
|||
private drawTrail(trail: LaserPointer, state: AppState): string {
|
||||
const stroke = trail
|
||||
.getStrokeOutline(trail.options.size / state.zoom.value)
|
||||
.map(([x, y]) => {
|
||||
const result = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x, sceneY: y },
|
||||
state,
|
||||
);
|
||||
|
||||
return [result.x, result.y];
|
||||
});
|
||||
.map((p) => sceneCoordsToViewportCoords(point(p[0], p[1]), state));
|
||||
|
||||
return getSvgPathFromStroke(stroke, true);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -164,8 +164,8 @@ export const EyeDropper: React.FC<{
|
|||
|
||||
// init color preview else it would show only after the first mouse move
|
||||
mouseMoveListener({
|
||||
clientX: stableProps.app.lastViewportPosition.x,
|
||||
clientY: stableProps.app.lastViewportPosition.y,
|
||||
clientX: stableProps.app.lastViewportPosition[0],
|
||||
clientY: stableProps.app.lastViewportPosition[1],
|
||||
altKey: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
} from "../../element/types";
|
||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
||||
import { point } from "../../../math";
|
||||
|
||||
type InteractiveCanvasProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
|
@ -103,10 +104,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||
remotePointerViewportCoords.set(
|
||||
socketId,
|
||||
sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: user.pointer.x,
|
||||
sceneY: user.pointer.y,
|
||||
},
|
||||
point(user.pointer.x, user.pointer.y),
|
||||
props.appState,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -36,7 +36,8 @@ import { trackEvent } from "../../analytics";
|
|||
import { useAppProps, useExcalidrawAppState } from "../App";
|
||||
import { isEmbeddableElement } from "../../element/typeChecks";
|
||||
import { getLinkHandleFromCoords } from "./helpers";
|
||||
import { point, type GlobalPoint } from "../../../math";
|
||||
import type { ViewportPoint } from "../../../math";
|
||||
import { point } from "../../../math";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
|
@ -324,8 +325,8 @@ const getCoordsForPopover = (
|
|||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1 + element.width / 2, sceneY: y1 },
|
||||
const [viewportX, viewportY] = sceneCoordsToViewportCoords(
|
||||
point(x1 + element.width / 2, y1),
|
||||
appState,
|
||||
);
|
||||
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
|
||||
|
@ -387,15 +388,15 @@ const renderTooltip = (
|
|||
);
|
||||
|
||||
const linkViewportCoords = sceneCoordsToViewportCoords(
|
||||
{ sceneX: linkX, sceneY: linkY },
|
||||
point(linkX, linkY),
|
||||
appState,
|
||||
);
|
||||
|
||||
updateTooltipPosition(
|
||||
tooltipDiv,
|
||||
{
|
||||
left: linkViewportCoords.x,
|
||||
top: linkViewportCoords.y,
|
||||
left: linkViewportCoords[0],
|
||||
top: linkViewportCoords[1],
|
||||
width: linkWidth,
|
||||
height: linkHeight,
|
||||
},
|
||||
|
@ -419,25 +420,22 @@ const shouldHideLinkPopup = (
|
|||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
[clientX, clientY]: GlobalPoint,
|
||||
viewportCoords: ViewportPoint,
|
||||
): Boolean => {
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{ clientX, clientY },
|
||||
appState,
|
||||
);
|
||||
const sceneCoords = viewportCoordsToSceneCoords(viewportCoords, appState);
|
||||
|
||||
const threshold = 15 / appState.zoom.value;
|
||||
// hitbox to prevent hiding when hovered in element bounding box
|
||||
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
|
||||
if (hitElementBoundingBox(sceneCoords, element, elementsMap)) {
|
||||
return false;
|
||||
}
|
||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
// hit box to prevent hiding when hovered in the vertical area between element and popover
|
||||
if (
|
||||
sceneX >= x1 &&
|
||||
sceneX <= x2 &&
|
||||
sceneY >= y1 - SPACE_BOTTOM &&
|
||||
sceneY <= y1
|
||||
sceneCoords[0] >= x1 &&
|
||||
sceneCoords[0] <= x2 &&
|
||||
sceneCoords[1] >= y1 - SPACE_BOTTOM &&
|
||||
sceneCoords[1] <= y1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
@ -449,10 +447,12 @@ const shouldHideLinkPopup = (
|
|||
);
|
||||
|
||||
if (
|
||||
clientX >= popoverX - threshold &&
|
||||
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
|
||||
clientY >= popoverY - threshold &&
|
||||
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
|
||||
viewportCoords[0] >= popoverX - threshold &&
|
||||
viewportCoords[0] <=
|
||||
popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
|
||||
viewportCoords[1] >= popoverY - threshold &&
|
||||
viewportCoords[1] <=
|
||||
popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ export const isPointHittingLink = (
|
|||
if (
|
||||
!isMobile &&
|
||||
appState.viewModeEnabled &&
|
||||
hitElementBoundingBox(x, y, element, elementsMap)
|
||||
hitElementBoundingBox(point(x, y), element, elementsMap)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { getElementAbsoluteCoords } from ".";
|
|||
import { useExcalidrawAppState } from "../components/App";
|
||||
|
||||
import "./ElementCanvasButtons.scss";
|
||||
import { point } from "../../math";
|
||||
|
||||
const CONTAINER_PADDING = 5;
|
||||
|
||||
|
@ -14,8 +15,8 @@ const getContainerCoords = (
|
|||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1 + element.width, sceneY: y1 },
|
||||
const [viewportX, viewportY] = sceneCoordsToViewportCoords(
|
||||
point(x1 + element.width, y1),
|
||||
appState,
|
||||
);
|
||||
const x = viewportX - appState.offsetLeft + 10;
|
||||
|
|
|
@ -49,7 +49,7 @@ import type { ElementUpdate } from "./mutateElement";
|
|||
import { mutateElement } from "./mutateElement";
|
||||
import type Scene from "../scene/Scene";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
||||
|
@ -389,7 +389,7 @@ export const getSuggestedBindingsForArrows = (
|
|||
export const maybeBindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
pointerCoords: { x: number; y: number },
|
||||
pointerCoords: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
): void => {
|
||||
|
@ -508,10 +508,7 @@ const unbindLinearElement = (
|
|||
};
|
||||
|
||||
export const getHoveredElementForBinding = (
|
||||
pointerCoords: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
pointer: GlobalPoint,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
fullShape?: boolean,
|
||||
|
@ -522,7 +519,7 @@ export const getHoveredElementForBinding = (
|
|||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(
|
||||
element,
|
||||
pointerCoords,
|
||||
pointer,
|
||||
elementsMap,
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
|
@ -1177,14 +1174,12 @@ const getLinearElementEdgeCoors = (
|
|||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
): { x: number; y: number } => {
|
||||
): GlobalPoint => {
|
||||
const index = startOrEnd === "start" ? 0 : -1;
|
||||
return tupleToCoors(
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
return LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
index,
|
||||
elementsMap,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1330,7 +1325,7 @@ const newBoundElements = (
|
|||
|
||||
export const bindingBorderTest = (
|
||||
element: NonDeleted<ExcalidrawBindableElement>,
|
||||
{ x, y }: { x: number; y: number },
|
||||
[x, y]: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
fullShape?: boolean,
|
||||
): boolean => {
|
||||
|
|
|
@ -42,35 +42,33 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
|||
};
|
||||
|
||||
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
||||
x: number;
|
||||
y: number;
|
||||
sceneCoords: Point;
|
||||
element: ExcalidrawElement;
|
||||
shape: GeometricShape<Point>;
|
||||
threshold?: number;
|
||||
frameNameBound?: FrameNameBounds | null;
|
||||
};
|
||||
|
||||
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||
x,
|
||||
y,
|
||||
export const hitElementItself = ({
|
||||
sceneCoords,
|
||||
element,
|
||||
shape,
|
||||
threshold = 10,
|
||||
frameNameBound = null,
|
||||
}: HitTestArgs<Point>) => {
|
||||
}: HitTestArgs<GlobalPoint>) => {
|
||||
let hit = shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInShape(point(x, y), shape) ||
|
||||
isPointOnShape(point(x, y), shape, threshold)
|
||||
: isPointOnShape(point(x, y), shape, threshold);
|
||||
isPointInShape(sceneCoords, shape) ||
|
||||
isPointOnShape(sceneCoords, shape, threshold)
|
||||
: isPointOnShape(sceneCoords, shape, threshold);
|
||||
|
||||
// hit test against a frame's name
|
||||
if (!hit && frameNameBound) {
|
||||
hit = isPointInShape(point(x, y), {
|
||||
hit = isPointInShape(sceneCoords, {
|
||||
type: "polygon",
|
||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||
.data as Polygon<Point>,
|
||||
.data as Polygon<GlobalPoint>,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -78,8 +76,7 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
|||
};
|
||||
|
||||
export const hitElementBoundingBox = (
|
||||
x: number,
|
||||
y: number,
|
||||
scenePointer: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
tolerance = 0,
|
||||
|
@ -89,31 +86,27 @@ export const hitElementBoundingBox = (
|
|||
y1 -= tolerance;
|
||||
x2 += tolerance;
|
||||
y2 += tolerance;
|
||||
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
||||
return isPointWithinBounds(point(x1, y1), scenePointer, point(x2, y2));
|
||||
};
|
||||
|
||||
export const hitElementBoundingBoxOnly = <
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
hitArgs: HitTestArgs<Point>,
|
||||
export const hitElementBoundingBoxOnly = (
|
||||
hitArgs: HitTestArgs<GlobalPoint>,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return (
|
||||
!hitElementItself(hitArgs) &&
|
||||
// bound text is considered part of the element (even if it's outside the bounding box)
|
||||
!hitElementBoundText(
|
||||
hitArgs.x,
|
||||
hitArgs.y,
|
||||
hitArgs.sceneCoords,
|
||||
getBoundTextShape(hitArgs.element, elementsMap),
|
||||
) &&
|
||||
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
|
||||
hitElementBoundingBox(hitArgs.sceneCoords, hitArgs.element, elementsMap)
|
||||
);
|
||||
};
|
||||
|
||||
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
||||
x: number,
|
||||
y: number,
|
||||
textShape: GeometricShape<Point> | null,
|
||||
export const hitElementBoundText = (
|
||||
scenePointer: GlobalPoint,
|
||||
textShape: GeometricShape<GlobalPoint> | null,
|
||||
): boolean => {
|
||||
return !!textShape && isPointInShape(point(x, y), textShape);
|
||||
return !!textShape && isPointInShape(scenePointer, textShape);
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
|||
Triangle,
|
||||
Vector,
|
||||
Radians,
|
||||
ViewportPoint,
|
||||
} from "../../math";
|
||||
import {
|
||||
point,
|
||||
|
@ -21,7 +22,9 @@ export const HEADING_LEFT = [-1, 0] as Heading;
|
|||
export const HEADING_UP = [0, -1] as Heading;
|
||||
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
||||
|
||||
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const headingForDiamond = <
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
) => {
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from "./bounds";
|
||||
import type {
|
||||
AppState,
|
||||
PointerCoords,
|
||||
InteractiveCanvasAppState,
|
||||
AppClassProperties,
|
||||
NullableGridSize,
|
||||
|
@ -32,7 +31,7 @@ import {
|
|||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
} from "./binding";
|
||||
import { invariant, toBrandedType, tupleToCoors } from "../utils";
|
||||
import { invariant, toBrandedType } from "../utils";
|
||||
import {
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
|
@ -56,6 +55,8 @@ import {
|
|||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
pointDistance,
|
||||
pointSubtract,
|
||||
pointFromPair,
|
||||
} from "../../math";
|
||||
import {
|
||||
getBezierCurveLength,
|
||||
|
@ -83,7 +84,7 @@ export class LinearElementEditor {
|
|||
/** index */
|
||||
lastClickedPoint: number;
|
||||
lastClickedIsEndPoint: boolean;
|
||||
origin: Readonly<{ x: number; y: number }> | null;
|
||||
origin: GlobalPoint | null;
|
||||
segmentMidpoint: {
|
||||
value: GlobalPoint | null;
|
||||
index: number | null;
|
||||
|
@ -94,7 +95,7 @@ export class LinearElementEditor {
|
|||
/** whether you're dragging a point */
|
||||
public readonly isDragging: boolean;
|
||||
public readonly lastUncommittedPoint: LocalPoint | null;
|
||||
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||
public readonly pointerOffset: GlobalPoint;
|
||||
public readonly startBindingElement:
|
||||
| ExcalidrawBindableElement
|
||||
| null
|
||||
|
@ -115,7 +116,7 @@ export class LinearElementEditor {
|
|||
this.selectedPointsIndices = null;
|
||||
this.lastUncommittedPoint = null;
|
||||
this.isDragging = false;
|
||||
this.pointerOffset = { x: 0, y: 0 };
|
||||
this.pointerOffset = point(0, 0);
|
||||
this.startBindingElement = "keep";
|
||||
this.endBindingElement = "keep";
|
||||
this.pointerDownState = {
|
||||
|
@ -219,11 +220,10 @@ export class LinearElementEditor {
|
|||
static handlePointDragging(
|
||||
event: PointerEvent,
|
||||
app: AppClassProperties,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scenePointer: GlobalPoint,
|
||||
maybeSuggestBinding: (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
pointSceneCoords: { x: number; y: number }[],
|
||||
pointSceneCoords: GlobalPoint[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scene: Scene,
|
||||
|
@ -287,7 +287,7 @@ export class LinearElementEditor {
|
|||
element,
|
||||
elementsMap,
|
||||
referencePoint,
|
||||
point(scenePointerX, scenePointerY),
|
||||
scenePointer,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
|
@ -309,8 +309,7 @@ export class LinearElementEditor {
|
|||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
pointSubtract(scenePointer, linearElementEditor.pointerOffset),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
|
@ -325,8 +324,10 @@ export class LinearElementEditor {
|
|||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
pointSubtract(
|
||||
scenePointer,
|
||||
linearElementEditor.pointerOffset,
|
||||
),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
)
|
||||
: point(
|
||||
|
@ -350,18 +351,16 @@ export class LinearElementEditor {
|
|||
|
||||
// suggest bindings for first and last point if selected
|
||||
if (isBindingElement(element, false)) {
|
||||
const coords: { x: number; y: number }[] = [];
|
||||
const coords: GlobalPoint[] = [];
|
||||
|
||||
const firstSelectedIndex = selectedPointsIndices[0];
|
||||
if (firstSelectedIndex === 0) {
|
||||
coords.push(
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[0],
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -369,13 +368,11 @@ export class LinearElementEditor {
|
|||
selectedPointsIndices[selectedPointsIndices.length - 1];
|
||||
if (lastSelectedIndex === element.points.length - 1) {
|
||||
coords.push(
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[lastSelectedIndex],
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -439,13 +436,11 @@ export class LinearElementEditor {
|
|||
|
||||
const bindingElement = isBindingEnabled(appState)
|
||||
? getHoveredElementForBinding(
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
selectedPoint!,
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
elements,
|
||||
elementsMap,
|
||||
)
|
||||
|
@ -481,7 +476,7 @@ export class LinearElementEditor {
|
|||
? [pointerDownState.lastClickedPoint]
|
||||
: selectedPointsIndices,
|
||||
isDragging: false,
|
||||
pointerOffset: { x: 0, y: 0 },
|
||||
pointerOffset: point(0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -556,7 +551,7 @@ export class LinearElementEditor {
|
|||
|
||||
static getSegmentMidpointHitCoords = (
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scenePointer: { x: number; y: number },
|
||||
scenePointer: GlobalPoint,
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint | null => {
|
||||
|
@ -569,8 +564,7 @@ export class LinearElementEditor {
|
|||
element,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
scenePointer,
|
||||
);
|
||||
if (clickedPointIndex >= 0) {
|
||||
return null;
|
||||
|
@ -594,7 +588,7 @@ export class LinearElementEditor {
|
|||
existingSegmentMidpointHitCoords[0],
|
||||
existingSegmentMidpointHitCoords[1],
|
||||
),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
scenePointer,
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return existingSegmentMidpointHitCoords;
|
||||
|
@ -607,7 +601,7 @@ export class LinearElementEditor {
|
|||
if (midPoints[index] !== null) {
|
||||
const distance = pointDistance(
|
||||
point(midPoints[index]![0], midPoints[index]![1]),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
scenePointer,
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return midPoints[index];
|
||||
|
@ -705,7 +699,7 @@ export class LinearElementEditor {
|
|||
event: React.PointerEvent<HTMLElement>,
|
||||
app: AppClassProperties,
|
||||
store: Store,
|
||||
scenePointer: { x: number; y: number },
|
||||
scenePointer: GlobalPoint,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scene: Scene,
|
||||
): {
|
||||
|
@ -759,8 +753,7 @@ export class LinearElementEditor {
|
|||
LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
scenePointer,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
),
|
||||
],
|
||||
|
@ -774,7 +767,7 @@ export class LinearElementEditor {
|
|||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: -1,
|
||||
lastClickedIsEndPoint: false,
|
||||
origin: { x: scenePointer.x, y: scenePointer.y },
|
||||
origin: scenePointer,
|
||||
segmentMidpoint: {
|
||||
value: segmentMidpoint,
|
||||
index: segmentMidpointIndex,
|
||||
|
@ -798,8 +791,7 @@ export class LinearElementEditor {
|
|||
element,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
scenePointer,
|
||||
);
|
||||
// if we clicked on a point, set the element as hitElement otherwise
|
||||
// it would get deselected if the point is outside the hitbox area
|
||||
|
@ -828,7 +820,7 @@ export class LinearElementEditor {
|
|||
const cy = (y1 + y2) / 2;
|
||||
const targetPoint =
|
||||
clickedPointIndex > -1 &&
|
||||
pointRotateRads(
|
||||
pointRotateRads<GlobalPoint>(
|
||||
point(
|
||||
element.x + element.points[clickedPointIndex][0],
|
||||
element.y + element.points[clickedPointIndex][1],
|
||||
|
@ -853,7 +845,7 @@ export class LinearElementEditor {
|
|||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: clickedPointIndex,
|
||||
lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1,
|
||||
origin: { x: scenePointer.x, y: scenePointer.y },
|
||||
origin: scenePointer,
|
||||
segmentMidpoint: {
|
||||
value: segmentMidpoint,
|
||||
index: segmentMidpointIndex,
|
||||
|
@ -862,11 +854,8 @@ export class LinearElementEditor {
|
|||
},
|
||||
selectedPointsIndices: nextSelectedPointsIndices,
|
||||
pointerOffset: targetPoint
|
||||
? {
|
||||
x: scenePointer.x - targetPoint[0],
|
||||
y: scenePointer.y - targetPoint[1],
|
||||
}
|
||||
: { x: 0, y: 0 },
|
||||
? pointSubtract(scenePointer, targetPoint)
|
||||
: point(0, 0),
|
||||
};
|
||||
|
||||
return ret;
|
||||
|
@ -887,8 +876,7 @@ export class LinearElementEditor {
|
|||
|
||||
static handlePointerMove(
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scenePointer: GlobalPoint,
|
||||
app: AppClassProperties,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
): LinearElementEditor | null {
|
||||
|
@ -928,7 +916,7 @@ export class LinearElementEditor {
|
|||
element,
|
||||
elementsMap,
|
||||
lastCommittedPoint,
|
||||
point(scenePointerX, scenePointerY),
|
||||
scenePointer,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
|
@ -940,8 +928,10 @@ export class LinearElementEditor {
|
|||
newPoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - appState.editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - appState.editingLinearElement.pointerOffset.y,
|
||||
pointSubtract(
|
||||
scenePointer,
|
||||
appState.editingLinearElement.pointerOffset,
|
||||
),
|
||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||
? null
|
||||
: app.getEffectiveGridSize(),
|
||||
|
@ -1057,8 +1047,7 @@ export class LinearElementEditor {
|
|||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: AppState["zoom"],
|
||||
x: number,
|
||||
y: number,
|
||||
p: GlobalPoint,
|
||||
) {
|
||||
const pointHandles = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
element,
|
||||
|
@ -1069,9 +1058,9 @@ export class LinearElementEditor {
|
|||
// points on the left, thus should take precedence when clicking, if they
|
||||
// overlap
|
||||
while (--idx > -1) {
|
||||
const p = pointHandles[idx];
|
||||
const handles = pointHandles[idx];
|
||||
if (
|
||||
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
|
||||
pointDistance(p, pointFromPair(handles)) * zoom.value <
|
||||
// +1px to account for outline stroke
|
||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||
) {
|
||||
|
@ -1084,11 +1073,14 @@ export class LinearElementEditor {
|
|||
static createPointAt(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scenePointer: GlobalPoint,
|
||||
gridSize: NullableGridSize,
|
||||
): LocalPoint {
|
||||
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
|
||||
const pointerOnGrid = getGridPoint(
|
||||
scenePointer[0],
|
||||
scenePointer[1],
|
||||
gridSize,
|
||||
);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
@ -1337,7 +1329,7 @@ export class LinearElementEditor {
|
|||
|
||||
static shouldAddMidpoint(
|
||||
linearElementEditor: LinearElementEditor,
|
||||
pointerCoords: PointerCoords,
|
||||
pointerCoords: GlobalPoint,
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
|
@ -1367,10 +1359,7 @@ export class LinearElementEditor {
|
|||
}
|
||||
|
||||
const origin = linearElementEditor.pointerDownState.origin!;
|
||||
const dist = pointDistance(
|
||||
point(origin.x, origin.y),
|
||||
point(pointerCoords.x, pointerCoords.y),
|
||||
);
|
||||
const dist = pointDistance(origin, pointerCoords);
|
||||
if (
|
||||
!appState.editingLinearElement &&
|
||||
dist < DRAGGING_THRESHOLD / appState.zoom.value
|
||||
|
@ -1382,7 +1371,7 @@ export class LinearElementEditor {
|
|||
|
||||
static addMidpoint(
|
||||
linearElementEditor: LinearElementEditor,
|
||||
pointerCoords: PointerCoords,
|
||||
pointerCoords: GlobalPoint,
|
||||
app: AppClassProperties,
|
||||
snapToGrid: boolean,
|
||||
elementsMap: ElementsMap,
|
||||
|
@ -1406,8 +1395,7 @@ export class LinearElementEditor {
|
|||
const midpoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
pointerCoords,
|
||||
snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null,
|
||||
);
|
||||
const points = [
|
||||
|
|
|
@ -1089,8 +1089,7 @@ export const getResizeOffsetXY = (
|
|||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
x: number,
|
||||
y: number,
|
||||
[x, y]: GlobalPoint,
|
||||
): [number, number] => {
|
||||
const [x1, y1, x2, y2] =
|
||||
selectedElements.length === 1
|
||||
|
|
|
@ -92,7 +92,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
|||
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
const sides = getSelectionBorders(
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point<Point>(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
element.angle,
|
||||
|
@ -101,7 +101,11 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
|||
for (const [dir, side] of Object.entries(sides)) {
|
||||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
||||
pointOnLineSegment(
|
||||
point<Point>(x, y),
|
||||
side as LineSegment<Point>,
|
||||
SPACING,
|
||||
)
|
||||
) {
|
||||
return dir as TransformHandleType;
|
||||
}
|
||||
|
@ -115,8 +119,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
|||
export const getElementWithTransformHandleType = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scenePointer: GlobalPoint,
|
||||
zoom: Zoom,
|
||||
pointerType: PointerType,
|
||||
elementsMap: ElementsMap,
|
||||
|
@ -130,8 +133,8 @@ export const getElementWithTransformHandleType = (
|
|||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
scenePointer[0],
|
||||
scenePointer[1],
|
||||
zoom,
|
||||
pointerType,
|
||||
device,
|
||||
|
@ -140,12 +143,9 @@ export const getElementWithTransformHandleType = (
|
|||
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
|
||||
};
|
||||
|
||||
export const getTransformHandleTypeFromCoords = <
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
export const getTransformHandleTypeFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scenePointer: GlobalPoint,
|
||||
zoom: Zoom,
|
||||
pointerType: PointerType,
|
||||
device: Device,
|
||||
|
@ -163,7 +163,7 @@ export const getTransformHandleTypeFromCoords = <
|
|||
transformHandles[key as Exclude<TransformHandleType, "rotation">]!;
|
||||
return (
|
||||
transformHandle &&
|
||||
isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY)
|
||||
isInsideTransformHandle(transformHandle, scenePointer[0], scenePointer[1])
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -178,7 +178,7 @@ export const getTransformHandleTypeFromCoords = <
|
|||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
|
||||
const sides = getSelectionBorders(
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point<GlobalPoint>(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
0 as Radians,
|
||||
|
@ -188,8 +188,8 @@ export const getTransformHandleTypeFromCoords = <
|
|||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(
|
||||
point(scenePointerX, scenePointerY),
|
||||
side as LineSegment<Point>,
|
||||
scenePointer,
|
||||
side as LineSegment<GlobalPoint>,
|
||||
SPACING,
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import BinaryHeap from "../binaryheap";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { aabbForElement, pointInsideBounds } from "../shapes";
|
||||
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
||||
import { isAnyTrue, toBrandedType } from "../utils";
|
||||
import {
|
||||
bindPointToSnapToElementOutline,
|
||||
distanceToBindableElement,
|
||||
|
@ -1081,13 +1081,13 @@ const getHoveredElements = (
|
|||
const elements = Array.from(elementsMap.values());
|
||||
return [
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(origStartGlobalPoint),
|
||||
origStartGlobalPoint,
|
||||
elements,
|
||||
nonDeletedSceneElementsMap,
|
||||
true,
|
||||
),
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(origEndGlobalPoint),
|
||||
origEndGlobalPoint,
|
||||
elements,
|
||||
nonDeletedSceneElementsMap,
|
||||
true,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|||
import type { AppState, Offsets, Zoom } from "../types";
|
||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||
import { viewportCoordsToSceneCoords } from "../utils";
|
||||
import { point } from "../../math";
|
||||
|
||||
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
||||
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
||||
|
@ -33,25 +34,22 @@ export const isElementInViewport = (
|
|||
) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
|
||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: viewTransformations.offsetLeft,
|
||||
clientY: viewTransformations.offsetTop,
|
||||
},
|
||||
point(viewTransformations.offsetLeft, viewTransformations.offsetTop),
|
||||
viewTransformations,
|
||||
);
|
||||
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: viewTransformations.offsetLeft + width,
|
||||
clientY: viewTransformations.offsetTop + height,
|
||||
},
|
||||
point(
|
||||
viewTransformations.offsetLeft + width,
|
||||
viewTransformations.offsetTop + height,
|
||||
),
|
||||
viewTransformations,
|
||||
);
|
||||
|
||||
return (
|
||||
topLeftSceneCoords.x <= x2 &&
|
||||
topLeftSceneCoords.y <= y2 &&
|
||||
bottomRightSceneCoords.x >= x1 &&
|
||||
bottomRightSceneCoords.y >= y1
|
||||
topLeftSceneCoords[0] <= x2 &&
|
||||
topLeftSceneCoords[1] <= y2 &&
|
||||
bottomRightSceneCoords[0] >= x1 &&
|
||||
bottomRightSceneCoords[1] >= y1
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -71,25 +69,25 @@ export const isElementCompletelyInViewport = (
|
|||
) => {
|
||||
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: viewTransformations.offsetLeft + (padding?.left || 0),
|
||||
clientY: viewTransformations.offsetTop + (padding?.top || 0),
|
||||
},
|
||||
point(
|
||||
viewTransformations.offsetLeft + (padding?.left || 0),
|
||||
viewTransformations.offsetTop + (padding?.top || 0),
|
||||
),
|
||||
viewTransformations,
|
||||
);
|
||||
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: viewTransformations.offsetLeft + width - (padding?.right || 0),
|
||||
clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0),
|
||||
},
|
||||
point(
|
||||
viewTransformations.offsetLeft + width - (padding?.right || 0),
|
||||
viewTransformations.offsetTop + height - (padding?.bottom || 0),
|
||||
),
|
||||
viewTransformations,
|
||||
);
|
||||
|
||||
return (
|
||||
x1 >= topLeftSceneCoords.x &&
|
||||
y1 >= topLeftSceneCoords.y &&
|
||||
x2 <= bottomRightSceneCoords.x &&
|
||||
y2 <= bottomRightSceneCoords.y
|
||||
x1 >= topLeftSceneCoords[0] &&
|
||||
y1 >= topLeftSceneCoords[1] &&
|
||||
x2 <= bottomRightSceneCoords[0] &&
|
||||
y2 <= bottomRightSceneCoords[1]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ import {
|
|||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import type { ExtractSetType } from "../utility-types";
|
||||
import type { GlobalPoint } from "../../math";
|
||||
import { point } from "../../math";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
|
@ -674,12 +676,12 @@ export const getContainerCenter = (
|
|||
container: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
): GlobalPoint => {
|
||||
if (!isArrowElement(container)) {
|
||||
return {
|
||||
x: container.x + container.width / 2,
|
||||
y: container.y + container.height / 2,
|
||||
};
|
||||
return point(
|
||||
container.x + container.width / 2,
|
||||
container.y + container.height / 2,
|
||||
);
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
container,
|
||||
|
@ -692,7 +694,7 @@ export const getContainerCenter = (
|
|||
container.points[index],
|
||||
elementsMap,
|
||||
);
|
||||
return { x: midPoint[0], y: midPoint[1] };
|
||||
return point(midPoint[0], midPoint[1]);
|
||||
}
|
||||
const index = container.points.length / 2 - 1;
|
||||
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
||||
|
@ -709,7 +711,7 @@ export const getContainerCenter = (
|
|||
elementsMap,
|
||||
);
|
||||
}
|
||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||
return point(midSegmentMidpoint[0], midSegmentMidpoint[1]);
|
||||
};
|
||||
|
||||
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||
|
|
|
@ -29,6 +29,7 @@ import { getElementLineSegments } from "./element/bounds";
|
|||
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
|
||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||
import type { ReadonlySetLike } from "./utility-types";
|
||||
import type { GlobalPoint } from "../math";
|
||||
import { isPointWithinBounds, point } from "../math";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
|
@ -149,20 +150,13 @@ export const elementOverlapsWithFrame = (
|
|||
};
|
||||
|
||||
export const isCursorInFrame = (
|
||||
cursorCoords: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
cursorCoords: GlobalPoint,
|
||||
frame: NonDeleted<ExcalidrawFrameLikeElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||
|
||||
return isPointWithinBounds(
|
||||
point(fx1, fy1),
|
||||
point(cursorCoords.x, cursorCoords.y),
|
||||
point(fx2, fy2),
|
||||
);
|
||||
return isPointWithinBounds(point(fx1, fy1), cursorCoords, point(fx2, fy2));
|
||||
};
|
||||
|
||||
export const groupsAreAtLeastIntersectingTheFrame = (
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import type { PointerCoords } from "./types";
|
||||
|
||||
export const getCenter = (pointers: Map<number, PointerCoords>) => {
|
||||
const allCoords = Array.from(pointers.values());
|
||||
return {
|
||||
x: sum(allCoords, (coords) => coords.x) / allCoords.length,
|
||||
y: sum(allCoords, (coords) => coords.y) / allCoords.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDistance = ([a, b]: readonly PointerCoords[]) =>
|
||||
Math.hypot(a.x - b.x, a.y - b.y);
|
||||
|
||||
const sum = <T>(array: readonly T[], mapper: (item: T) => number): number =>
|
||||
array.reduce((acc, item) => acc + mapper(item), 0);
|
|
@ -26,7 +26,7 @@ import type {
|
|||
RenderableElementsMap,
|
||||
InteractiveCanvasRenderConfig,
|
||||
} from "../scene/types";
|
||||
import { distance, getFontString, isRTL } from "../utils";
|
||||
import { getFontString, isRTL } from "../utils";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import type {
|
||||
AppState,
|
||||
|
@ -59,7 +59,7 @@ import { LinearElementEditor } from "../element/linearElementEditor";
|
|||
import { getContainingFrame } from "../frame";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { getVerticalOffset } from "../fonts";
|
||||
import { isRightAngleRads } from "../../math";
|
||||
import { isRightAngleRads, rangeExtent, rangeInclusive } from "../../math";
|
||||
import { getCornerRadius } from "../shapes";
|
||||
|
||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||
|
@ -163,11 +163,11 @@ const cappedElementCanvasSize = (
|
|||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const elementWidth =
|
||||
isLinearElement(element) || isFreeDrawElement(element)
|
||||
? distance(x1, x2)
|
||||
? rangeExtent(rangeInclusive(x1, x2))
|
||||
: element.width;
|
||||
const elementHeight =
|
||||
isLinearElement(element) || isFreeDrawElement(element)
|
||||
? distance(y1, y2)
|
||||
? rangeExtent(rangeInclusive(y1, y2))
|
||||
: element.height;
|
||||
|
||||
let width = elementWidth * window.devicePixelRatio + padding * 2;
|
||||
|
@ -226,12 +226,16 @@ const generateElementCanvas = (
|
|||
|
||||
canvasOffsetX =
|
||||
element.x > x1
|
||||
? distance(element.x, x1) * window.devicePixelRatio * scale
|
||||
? rangeExtent(rangeInclusive(element.x, x1)) *
|
||||
window.devicePixelRatio *
|
||||
scale
|
||||
: 0;
|
||||
|
||||
canvasOffsetY =
|
||||
element.y > y1
|
||||
? distance(element.y, y1) * window.devicePixelRatio * scale
|
||||
? rangeExtent(rangeInclusive(element.y, y1)) *
|
||||
window.devicePixelRatio *
|
||||
scale
|
||||
: 0;
|
||||
|
||||
context.translate(canvasOffsetX, canvasOffsetY);
|
||||
|
@ -263,7 +267,10 @@ const generateElementCanvas = (
|
|||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
// Take max dimensions of arrow canvas so that when canvas is rotated
|
||||
// the arrow doesn't get clipped
|
||||
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
|
||||
const maxDim = Math.max(
|
||||
rangeExtent(rangeInclusive(x1, x2)),
|
||||
rangeExtent(rangeInclusive(y1, y2)),
|
||||
);
|
||||
boundTextCanvas.width =
|
||||
maxDim * window.devicePixelRatio * scale + padding * scale * 10;
|
||||
boundTextCanvas.height =
|
||||
|
@ -813,7 +820,10 @@ export const renderElement = (
|
|||
|
||||
// Take max dimensions of arrow canvas so that when canvas is rotated
|
||||
// the arrow doesn't get clipped
|
||||
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
|
||||
const maxDim = Math.max(
|
||||
rangeExtent(rangeInclusive(x1, x2)),
|
||||
rangeExtent(rangeInclusive(y1, y2)),
|
||||
);
|
||||
const padding = getCanvasPadding(element);
|
||||
tempCanvas.width =
|
||||
maxDim * appState.exportScale + padding * 10 * appState.exportScale;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ViewportPoint } from "../../math";
|
||||
import { point, type GlobalPoint, type LocalPoint } from "../../math";
|
||||
import { THEME } from "../constants";
|
||||
import type { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||
|
@ -107,7 +108,7 @@ const drawCross = <Point extends LocalPoint | GlobalPoint>(
|
|||
context.restore();
|
||||
};
|
||||
|
||||
const drawLine = <Point extends LocalPoint | GlobalPoint>(
|
||||
const drawLine = <Point extends LocalPoint | GlobalPoint | ViewportPoint>(
|
||||
from: Point,
|
||||
to: Point,
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -118,7 +119,7 @@ const drawLine = <Point extends LocalPoint | GlobalPoint>(
|
|||
context.stroke();
|
||||
};
|
||||
|
||||
const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
||||
const drawGapLine = <Point extends LocalPoint | GlobalPoint | ViewportPoint>(
|
||||
from: Point,
|
||||
to: Point,
|
||||
direction: "horizontal" | "vertical",
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
import type { Bounds } from "../element/bounds";
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
||||
import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
|
||||
import { arrayToMap, getFontString, toBrandedType } from "../utils";
|
||||
import type { AppState, BinaryFiles } from "../types";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
|
@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
|
|||
import { renderStaticScene } from "../renderer/staticScene";
|
||||
import { Fonts } from "../fonts";
|
||||
import type { Font } from "../fonts/ExcalidrawFont";
|
||||
import { rangeExtent, rangeInclusive } from "../../math";
|
||||
|
||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
|
@ -427,8 +428,8 @@ const getCanvasSize = (
|
|||
exportPadding: number,
|
||||
): Bounds => {
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||
const width = distance(minX, maxX) + exportPadding * 2;
|
||||
const height = distance(minY, maxY) + exportPadding * 2;
|
||||
const width = rangeExtent(rangeInclusive(minX, maxX)) + exportPadding * 2;
|
||||
const height = rangeExtent(rangeInclusive(minY, maxY)) + exportPadding * 2;
|
||||
|
||||
return [minX, minY, width, height];
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
|
||||
import type { AppState, Offsets, Zoom } from "../types";
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
getCommonBounds,
|
||||
|
@ -8,17 +8,19 @@ import {
|
|||
|
||||
import {
|
||||
sceneCoordsToViewportCoords,
|
||||
tupleToCoors,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "../utils";
|
||||
import { point, type GlobalPoint } from "../../math";
|
||||
|
||||
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
|
||||
const [x1, y1, x2, y2] = cords;
|
||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1, sceneY: y1 },
|
||||
const [viewportX1, viewportY1] = sceneCoordsToViewportCoords(
|
||||
point(x1, y1),
|
||||
appState,
|
||||
);
|
||||
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x2, sceneY: y2 },
|
||||
const [viewportX2, viewportY2] = sceneCoordsToViewportCoords(
|
||||
point(x2, y2),
|
||||
appState,
|
||||
);
|
||||
return (
|
||||
|
@ -33,20 +35,20 @@ export const centerScrollOn = ({
|
|||
zoom,
|
||||
offsets,
|
||||
}: {
|
||||
scenePoint: PointerCoords;
|
||||
scenePoint: GlobalPoint;
|
||||
viewportDimensions: { height: number; width: number };
|
||||
zoom: Zoom;
|
||||
offsets?: Offsets;
|
||||
}) => {
|
||||
let scrollX =
|
||||
(viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value -
|
||||
scenePoint.x;
|
||||
scenePoint[0];
|
||||
|
||||
scrollX += (offsets?.left ?? 0) / 2 / zoom.value;
|
||||
|
||||
let scrollY =
|
||||
(viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value -
|
||||
scenePoint.y;
|
||||
scenePoint[1];
|
||||
|
||||
scrollY += (offsets?.top ?? 0) / 2 / zoom.value;
|
||||
|
||||
|
@ -73,10 +75,12 @@ export const calculateScrollCenter = (
|
|||
if (isOutsideViewPort(appState, [x1, y1, x2, y2])) {
|
||||
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||
elements,
|
||||
tupleToCoors(
|
||||
viewportCoordsToSceneCoords(
|
||||
{ clientX: appState.scrollX, clientY: appState.scrollY },
|
||||
point(appState.scrollX, appState.scrollY),
|
||||
appState,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -84,7 +88,7 @@ export const calculateScrollCenter = (
|
|||
const centerY = (y1 + y2) / 2;
|
||||
|
||||
return centerScrollOn({
|
||||
scenePoint: { x: centerX, y: centerY },
|
||||
scenePoint: point(centerX, centerY),
|
||||
viewportDimensions: { width: appState.width, height: appState.height },
|
||||
zoom: appState.zoom,
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
PendingExcalidrawElements,
|
||||
} from "../types";
|
||||
import type { MakeBrand } from "../utility-types";
|
||||
import type { ViewportPoint } from "../../math";
|
||||
|
||||
export type RenderableElementsMap = NonDeletedElementsMap &
|
||||
MakeBrand<"RenderableElementsMap">;
|
||||
|
@ -52,7 +53,7 @@ export type InteractiveCanvasRenderConfig = {
|
|||
// collab-related state
|
||||
// ---------------------------------------------------------------------------
|
||||
remoteSelectedElementIds: Map<ExcalidrawElement["id"], SocketId[]>;
|
||||
remotePointerViewportCoords: Map<SocketId, { x: number; y: number }>;
|
||||
remotePointerViewportCoords: Map<SocketId, ViewportPoint>;
|
||||
remotePointerUserStates: Map<SocketId, UserIdleState>;
|
||||
remotePointerUsernames: Map<SocketId, string>;
|
||||
remotePointerButton: Map<SocketId, string | undefined>;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ViewportPoint } from "../math";
|
||||
import {
|
||||
isPoint,
|
||||
point,
|
||||
|
@ -435,7 +436,9 @@ export const aabbForElement = (
|
|||
return bounds;
|
||||
};
|
||||
|
||||
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
|
||||
export const pointInsideBounds = <
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
p: P,
|
||||
bounds: Bounds,
|
||||
): boolean =>
|
||||
|
|
|
@ -41,6 +41,7 @@ import type { ContextMenuItems } from "./components/ContextMenu";
|
|||
import type { SnapLine } from "./snapping";
|
||||
import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types";
|
||||
import type { StoreActionType } from "./store";
|
||||
import type { GlobalPoint } from "../math";
|
||||
|
||||
export type SocketId = string & { _brand: "SocketId" };
|
||||
|
||||
|
@ -415,14 +416,9 @@ export type Zoom = Readonly<{
|
|||
value: NormalizedZoomValue;
|
||||
}>;
|
||||
|
||||
export type PointerCoords = Readonly<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
|
||||
export type Gesture = {
|
||||
pointers: Map<number, PointerCoords>;
|
||||
lastCenter: { x: number; y: number } | null;
|
||||
pointers: Map<number, GlobalPoint>;
|
||||
lastCenter: GlobalPoint | null;
|
||||
initialDistance: number | null;
|
||||
initialScale: number | null;
|
||||
};
|
||||
|
@ -661,17 +657,17 @@ export type AppClassProperties = {
|
|||
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||
};
|
||||
|
||||
export type PointerDownState = Readonly<{
|
||||
export type PointerDownState = {
|
||||
// The first position at which pointerDown happened
|
||||
origin: Readonly<{ x: number; y: number }>;
|
||||
origin: Readonly<GlobalPoint>;
|
||||
// Same as "origin" but snapped to the grid, if grid is on
|
||||
originInGrid: Readonly<{ x: number; y: number }>;
|
||||
// Scrollbar checks
|
||||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
||||
scrollbars: Readonly<ReturnType<typeof isOverScrollBars>>;
|
||||
// The previous pointer position
|
||||
lastCoords: { x: number; y: number };
|
||||
lastCoords: GlobalPoint;
|
||||
// map of original elements data
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||
originalElements: Readonly<Map<string, NonDeleted<ExcalidrawElement>>>;
|
||||
resize: {
|
||||
// Handle when resizing, might change during the pointer interaction
|
||||
handleType: MaybeTransformHandleType;
|
||||
|
@ -698,12 +694,12 @@ export type PointerDownState = Readonly<{
|
|||
hasBeenDuplicated: boolean;
|
||||
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
||||
};
|
||||
withCmdOrCtrl: boolean;
|
||||
withCmdOrCtrl: Readonly<boolean>;
|
||||
drag: {
|
||||
// Might change during the pointer interaction
|
||||
hasOccurred: boolean;
|
||||
// Might change during the pointer interaction
|
||||
offset: { x: number; y: number } | null;
|
||||
offset: Readonly<{ x: number; y: number }> | null;
|
||||
};
|
||||
// We need to have these in the state so that we can unsubscribe them
|
||||
eventListeners: {
|
||||
|
@ -719,7 +715,7 @@ export type PointerDownState = Readonly<{
|
|||
boxSelection: {
|
||||
hasOccurred: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export type UnsubscribeCallback = () => void;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { average } from "../math";
|
||||
import type { GlobalPoint, ViewportPoint } from "../math";
|
||||
import { average, point } from "../math";
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
import type { EVENT } from "./constants";
|
||||
import {
|
||||
|
@ -363,8 +364,6 @@ export const removeSelection = () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const distance = (x: number, y: number) => Math.abs(x - y);
|
||||
|
||||
export const updateActiveTool = (
|
||||
appState: Pick<AppState, "activeTool">,
|
||||
data: ((
|
||||
|
@ -419,7 +418,7 @@ export const getShortcutKey = (shortcut: string): string => {
|
|||
};
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
[clientX, clientY]: ViewportPoint,
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
|
@ -433,15 +432,15 @@ export const viewportCoordsToSceneCoords = (
|
|||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
): GlobalPoint => {
|
||||
const x = (clientX - offsetLeft) / zoom.value - scrollX;
|
||||
const y = (clientY - offsetTop) / zoom.value - scrollY;
|
||||
|
||||
return { x, y };
|
||||
return point<GlobalPoint>(x, y);
|
||||
};
|
||||
|
||||
export const sceneCoordsToViewportCoords = (
|
||||
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
|
||||
[sceneX, sceneY]: GlobalPoint,
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
|
@ -455,10 +454,10 @@ export const sceneCoordsToViewportCoords = (
|
|||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
): ViewportPoint => {
|
||||
const x = (sceneX + scrollX) * zoom.value + offsetLeft;
|
||||
const y = (sceneY + scrollY) * zoom.value + offsetTop;
|
||||
return { x, y };
|
||||
return point(x, y);
|
||||
};
|
||||
|
||||
export const getGlobalCSSVariable = (name: string) =>
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
|||
LocalPoint,
|
||||
PolarCoords,
|
||||
Radians,
|
||||
ViewportPoint,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
|
@ -23,10 +24,9 @@ export const normalizeRadians = (angle: Radians): Radians => {
|
|||
* (x, y) for the center point 0,0 where the first number returned is the radius,
|
||||
* the second is the angle in radians.
|
||||
*/
|
||||
export const cartesian2Polar = <P extends GlobalPoint | LocalPoint>([
|
||||
x,
|
||||
y,
|
||||
]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
|
||||
export const cartesian2Polar = <
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>([x, y]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
|
||||
|
||||
export function degreesToRadians(degrees: Degrees): Radians {
|
||||
return ((degrees * Math.PI) / 180) as Radians;
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import { cartesian2Polar } from "./angle";
|
||||
import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types";
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LocalPoint,
|
||||
SymmetricArc,
|
||||
ViewportPoint,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
/**
|
||||
* Determines if a cartesian point lies on a symmetric arc, i.e. an arc which
|
||||
* is part of a circle contour centered on 0, 0.
|
||||
*/
|
||||
export const isPointOnSymmetricArc = <P extends GlobalPoint | LocalPoint>(
|
||||
export const isPointOnSymmetricArc = <
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
||||
point: P,
|
||||
): boolean => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
Radians,
|
||||
Degrees,
|
||||
Vector,
|
||||
ViewportPoint,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
import { vectorFromPoint, vectorScale } from "./vector";
|
||||
|
@ -16,7 +17,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
|
|||
* @param y The Y coordinate
|
||||
* @returns The branded and created point
|
||||
*/
|
||||
export function point<Point extends GlobalPoint | LocalPoint>(
|
||||
export function point<Point extends GlobalPoint | LocalPoint | ViewportPoint>(
|
||||
x: number,
|
||||
y: number,
|
||||
): Point {
|
||||
|
@ -29,9 +30,9 @@ export function point<Point extends GlobalPoint | LocalPoint>(
|
|||
* @param numberArray The number array to check and to convert to Point
|
||||
* @returns The point instance
|
||||
*/
|
||||
export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
|
||||
numberArray: number[],
|
||||
): Point | undefined {
|
||||
export function pointFromArray<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(numberArray: number[]): Point | undefined {
|
||||
return numberArray.length === 2
|
||||
? point<Point>(numberArray[0], numberArray[1])
|
||||
: undefined;
|
||||
|
@ -43,9 +44,9 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
|
|||
* @param pair A number pair to convert to Point
|
||||
* @returns The point instance
|
||||
*/
|
||||
export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
|
||||
pair: [number, number],
|
||||
): Point {
|
||||
export function pointFromPair<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(pair: [number, number]): Point {
|
||||
return pair as Point;
|
||||
}
|
||||
|
||||
|
@ -55,9 +56,9 @@ export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
|
|||
* @param v The vector to convert
|
||||
* @returns The point the vector points at with origin 0,0
|
||||
*/
|
||||
export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
||||
v: Vector,
|
||||
): P {
|
||||
export function pointFromVector<
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(v: Vector): P {
|
||||
return v as unknown as P;
|
||||
}
|
||||
|
||||
|
@ -67,7 +68,9 @@ export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
|||
* @param p The value to attempt verification on
|
||||
* @returns TRUE if the provided value has the shape of a local or global point
|
||||
*/
|
||||
export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
||||
export function isPoint(
|
||||
p: unknown,
|
||||
): p is LocalPoint | GlobalPoint | ViewportPoint {
|
||||
return (
|
||||
Array.isArray(p) &&
|
||||
p.length === 2 &&
|
||||
|
@ -86,10 +89,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
|||
* @param b Point The second point to compare
|
||||
* @returns TRUE if the points are sufficiently close to each other
|
||||
*/
|
||||
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): boolean {
|
||||
export function pointsEqual<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(a: Point, b: Point): boolean {
|
||||
const abs = Math.abs;
|
||||
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
||||
}
|
||||
|
@ -102,11 +104,9 @@ export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
|||
* @param angle The radians to rotate the point by
|
||||
* @returns The rotated point
|
||||
*/
|
||||
export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
|
||||
[x, y]: Point,
|
||||
[cx, cy]: Point,
|
||||
angle: Radians,
|
||||
): Point {
|
||||
export function pointRotateRads<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>([x, y]: Point, [cx, cy]: Point, angle: Radians): Point {
|
||||
return point(
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||
|
@ -121,11 +121,9 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
|
|||
* @param angle The degree to rotate the point by
|
||||
* @returns The rotated point
|
||||
*/
|
||||
export function pointRotateDegs<Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
center: Point,
|
||||
angle: Degrees,
|
||||
): Point {
|
||||
export function pointRotateDegs<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(point: Point, center: Point, angle: Degrees): Point {
|
||||
return pointRotateRads(point, center, degreesToRadians(angle));
|
||||
}
|
||||
|
||||
|
@ -143,8 +141,8 @@ export function pointRotateDegs<Point extends GlobalPoint | LocalPoint>(
|
|||
*/
|
||||
// TODO 99% of use is translating between global and local coords, which need to be formalized
|
||||
export function pointTranslate<
|
||||
From extends GlobalPoint | LocalPoint,
|
||||
To extends GlobalPoint | LocalPoint,
|
||||
From extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
To extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(p: From, v: Vector = [0, 0] as Vector): To {
|
||||
return point(p[0] + v[0], p[1] + v[1]);
|
||||
}
|
||||
|
@ -156,8 +154,14 @@ export function pointTranslate<
|
|||
* @param b The other point to create the middle point for
|
||||
* @returns The middle point
|
||||
*/
|
||||
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
||||
return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
||||
export function pointCenter<P extends LocalPoint | GlobalPoint | ViewportPoint>(
|
||||
...p: P[]
|
||||
): P {
|
||||
return pointFromPair(
|
||||
p
|
||||
.reduce((mid, x) => [mid[0] + x[0], mid[1] + x[1]], [0, 0])
|
||||
.map((x) => x / p.length) as [number, number],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,10 +172,9 @@ export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
|||
* @param b The other point to act like the vector to translate by
|
||||
* @returns
|
||||
*/
|
||||
export function pointAdd<Point extends LocalPoint | GlobalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): Point {
|
||||
export function pointAdd<
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(a: Point, b: Point): Point {
|
||||
return point(a[0] + b[0], a[1] + b[1]);
|
||||
}
|
||||
|
||||
|
@ -183,10 +186,9 @@ export function pointAdd<Point extends LocalPoint | GlobalPoint>(
|
|||
* @param b The point which will act like a vector
|
||||
* @returns The resulting point
|
||||
*/
|
||||
export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
): Point {
|
||||
export function pointSubtract<
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(a: Point, b: Point): Point {
|
||||
return point(a[0] - b[0], a[1] - b[1]);
|
||||
}
|
||||
|
||||
|
@ -197,10 +199,9 @@ export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
|
|||
* @param b Second point
|
||||
* @returns The euclidean distance between the two points.
|
||||
*/
|
||||
export function pointDistance<P extends LocalPoint | GlobalPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): number {
|
||||
export function pointDistance<
|
||||
P extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(a: P, b: P): number {
|
||||
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
}
|
||||
|
||||
|
@ -213,10 +214,9 @@ export function pointDistance<P extends LocalPoint | GlobalPoint>(
|
|||
* @param b Second point
|
||||
* @returns The euclidean distance between the two points.
|
||||
*/
|
||||
export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): number {
|
||||
export function pointDistanceSq<
|
||||
P extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(a: P, b: P): number {
|
||||
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
}
|
||||
|
||||
|
@ -228,7 +228,9 @@ export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
|
|||
* @param multiplier The scaling factor
|
||||
* @returns
|
||||
*/
|
||||
export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
|
||||
export const pointScaleFromOrigin = <
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
p: P,
|
||||
mid: P,
|
||||
multiplier: number,
|
||||
|
@ -243,7 +245,9 @@ export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
|
|||
* @param r The other point to compare against
|
||||
* @returns TRUE if q is indeed between p and r
|
||||
*/
|
||||
export const isPointWithinBounds = <P extends GlobalPoint | LocalPoint>(
|
||||
export const isPointWithinBounds = <
|
||||
P extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
p: P,
|
||||
q: P,
|
||||
r: P,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { pointsEqual } from "./point";
|
||||
import { lineSegment, pointOnLineSegment } from "./segment";
|
||||
import type { GlobalPoint, LocalPoint, Polygon } from "./types";
|
||||
import type { GlobalPoint, LocalPoint, Polygon, ViewportPoint } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
export function polygon<Point extends GlobalPoint | LocalPoint>(
|
||||
|
@ -9,13 +9,15 @@ export function polygon<Point extends GlobalPoint | LocalPoint>(
|
|||
return polygonClose(points) as Polygon<Point>;
|
||||
}
|
||||
|
||||
export function polygonFromPoints<Point extends GlobalPoint | LocalPoint>(
|
||||
points: Point[],
|
||||
) {
|
||||
export function polygonFromPoints<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(points: Point[]) {
|
||||
return polygonClose(points) as Polygon<Point>;
|
||||
}
|
||||
|
||||
export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const polygonIncludesPoint = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
polygon: Polygon<Point>,
|
||||
) => {
|
||||
|
@ -40,7 +42,9 @@ export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
|||
return inside;
|
||||
};
|
||||
|
||||
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointOnPolygon = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
p: Point,
|
||||
poly: Polygon<Point>,
|
||||
threshold = PRECISION,
|
||||
|
@ -57,7 +61,7 @@ export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
|||
return on;
|
||||
};
|
||||
|
||||
function polygonClose<Point extends LocalPoint | GlobalPoint>(
|
||||
function polygonClose<Point extends LocalPoint | GlobalPoint | ViewportPoint>(
|
||||
polygon: Point[],
|
||||
) {
|
||||
return polygonIsClosed(polygon)
|
||||
|
@ -65,8 +69,8 @@ function polygonClose<Point extends LocalPoint | GlobalPoint>(
|
|||
: ([...polygon, polygon[0]] as Polygon<Point>);
|
||||
}
|
||||
|
||||
function polygonIsClosed<Point extends LocalPoint | GlobalPoint>(
|
||||
polygon: Point[],
|
||||
) {
|
||||
function polygonIsClosed<
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(polygon: Point[]) {
|
||||
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
||||
}
|
||||
|
|
|
@ -71,8 +71,8 @@ export const rangeIntersection = (
|
|||
* Determine if a value is inside a range.
|
||||
*
|
||||
* @param value The value to check
|
||||
* @param range The range
|
||||
* @returns
|
||||
* @param range The range to check
|
||||
* @returns TRUE if the value is in range
|
||||
*/
|
||||
export const rangeIncludesValue = (
|
||||
value: number,
|
||||
|
@ -80,3 +80,13 @@ export const rangeIncludesValue = (
|
|||
): boolean => {
|
||||
return value >= min && value <= max;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the distance between the start and end of the range.
|
||||
*
|
||||
* @param range The range of which to measure the extent of
|
||||
* @returns The scalar distance or extent of the start and end of the range
|
||||
*/
|
||||
export function rangeExtent([a, b]: InclusiveRange) {
|
||||
return Math.abs(a - b);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,13 @@ import {
|
|||
pointFromVector,
|
||||
pointRotateRads,
|
||||
} from "./point";
|
||||
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Radians,
|
||||
ViewportPoint,
|
||||
} from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
import {
|
||||
vectorAdd,
|
||||
|
@ -20,7 +26,7 @@ import {
|
|||
* @param points The two points delimiting the line segment on each end
|
||||
* @returns The line segment delineated by the points
|
||||
*/
|
||||
export function lineSegment<P extends GlobalPoint | LocalPoint>(
|
||||
export function lineSegment<P extends GlobalPoint | LocalPoint | ViewportPoint>(
|
||||
a: P,
|
||||
b: P,
|
||||
): LineSegment<P> {
|
||||
|
@ -57,7 +63,9 @@ export const isLineSegment = <Point extends GlobalPoint | LocalPoint>(
|
|||
* @param origin
|
||||
* @returns
|
||||
*/
|
||||
export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const lineSegmentRotate = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
l: LineSegment<Point>,
|
||||
angle: Radians,
|
||||
origin?: Point,
|
||||
|
@ -72,7 +80,9 @@ export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
|
|||
* Calculates the point two line segments with a definite start and end point
|
||||
* intersect at.
|
||||
*/
|
||||
export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const segmentsIntersectAt = <
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
a: Readonly<LineSegment<Point>>,
|
||||
b: Readonly<LineSegment<Point>>,
|
||||
): Point | null => {
|
||||
|
@ -105,7 +115,9 @@ export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
|||
return null;
|
||||
};
|
||||
|
||||
export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointOnLineSegment = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
line: LineSegment<Point>,
|
||||
threshold = PRECISION,
|
||||
|
@ -119,7 +131,9 @@ export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
|||
return distance < threshold;
|
||||
};
|
||||
|
||||
export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const distanceToLineSegment = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
line: LineSegment<Point>,
|
||||
) => {
|
||||
|
|
|
@ -43,6 +43,13 @@ export type LocalPoint = [x: number, y: number] & {
|
|||
_brand: "excalimath__localpoint";
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a 2D position on the browser viewport.
|
||||
*/
|
||||
export type ViewportPoint = [x: number, y: number] & {
|
||||
_brand: "excalimath_viewportpoint";
|
||||
};
|
||||
|
||||
// Line
|
||||
|
||||
/**
|
||||
|
@ -57,7 +64,10 @@ export type Line<P extends GlobalPoint | LocalPoint> = [p: P, q: P] & {
|
|||
* line that is bounded by two distinct end points, and
|
||||
* contains every point on the line that is between its endpoints.
|
||||
*/
|
||||
export type LineSegment<P extends GlobalPoint | LocalPoint> = [a: P, b: P] & {
|
||||
export type LineSegment<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
|
||||
a: P,
|
||||
b: P,
|
||||
] & {
|
||||
_brand: "excalimath_linesegment";
|
||||
};
|
||||
|
||||
|
@ -93,7 +103,8 @@ export type Triangle<P extends GlobalPoint | LocalPoint> = [
|
|||
* A polygon is a closed shape by connecting the given points
|
||||
* rectangles and diamonds are modelled by polygons
|
||||
*/
|
||||
export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
|
||||
export type Polygon<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||
Point[] & {
|
||||
_brand: "excalimath_polygon";
|
||||
};
|
||||
|
||||
|
@ -104,7 +115,7 @@ export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
|
|||
/**
|
||||
* Cubic bezier curve with four control points
|
||||
*/
|
||||
export type Curve<Point extends GlobalPoint | LocalPoint> = [
|
||||
export type Curve<Point extends GlobalPoint | LocalPoint | ViewportPoint> = [
|
||||
Point,
|
||||
Point,
|
||||
Point,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { GlobalPoint, LocalPoint, Vector } from "./types";
|
||||
import type { GlobalPoint, LocalPoint, Vector, ViewportPoint } from "./types";
|
||||
|
||||
/**
|
||||
* Create a vector from the x and y coordiante elements.
|
||||
|
@ -23,10 +23,9 @@ export function vector(
|
|||
* @param origin The origin point in a given coordiante system
|
||||
* @returns The created vector from the point and the origin
|
||||
*/
|
||||
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
p: Point,
|
||||
origin: Point = [0, 0] as Point,
|
||||
): Vector {
|
||||
export function vectorFromPoint<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(p: Point, origin: Point = [0, 0] as Point): Vector {
|
||||
return vector(p[0] - origin[0], p[1] - origin[1]);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
pointOnEllipse,
|
||||
type GeometricShape,
|
||||
} from "./geometry/shape";
|
||||
import type { Curve } from "../math";
|
||||
import type { Curve, ViewportPoint } from "../math";
|
||||
import {
|
||||
lineSegment,
|
||||
point,
|
||||
|
@ -18,7 +18,9 @@ import {
|
|||
} from "../math";
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const isPointOnShape = <
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
tolerance = 0,
|
||||
|
@ -45,21 +47,21 @@ export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
|||
|
||||
// check if the given point is considered inside the element's border
|
||||
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
p: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
) => {
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return polygonIncludesPoint(point, shape.data);
|
||||
return polygonIncludesPoint(p, shape.data);
|
||||
case "line":
|
||||
return false;
|
||||
case "curve":
|
||||
return false;
|
||||
case "ellipse":
|
||||
return pointInEllipse(point, shape.data);
|
||||
return pointInEllipse(p, shape.data);
|
||||
case "polyline": {
|
||||
const polygon = polygonFromPoints(shape.data.flat());
|
||||
return polygonIncludesPoint(point, polygon);
|
||||
return polygonIncludesPoint(p, polygon);
|
||||
}
|
||||
case "polycurve": {
|
||||
return false;
|
||||
|
@ -77,7 +79,9 @@ export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
|
|||
return polygonIncludesPoint(point, bounds);
|
||||
};
|
||||
|
||||
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
const pointOnPolycurve = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
polycurve: Polycurve<Point>,
|
||||
tolerance: number,
|
||||
|
@ -85,7 +89,9 @@ const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
|||
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
|
||||
};
|
||||
|
||||
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
||||
const cubicBezierEquation = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
curve: Curve<Point>,
|
||||
) => {
|
||||
const [p0, p1, p2, p3] = curve;
|
||||
|
@ -97,7 +103,9 @@ const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
|||
p0[idx] * Math.pow(t, 3);
|
||||
};
|
||||
|
||||
const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
const polyLineFromCurve = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
curve: Curve<Point>,
|
||||
segments = 10,
|
||||
): Polyline<Point> => {
|
||||
|
@ -119,7 +127,9 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
|||
return lineSegments;
|
||||
};
|
||||
|
||||
export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointOnCurve = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
curve: Curve<Point>,
|
||||
threshold: number,
|
||||
|
@ -127,7 +137,9 @@ export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
|
|||
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
|
||||
};
|
||||
|
||||
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointOnPolyline = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
polyline: Polyline<Point>,
|
||||
threshold = 10e-5,
|
||||
|
|
|
@ -11,18 +11,6 @@ import {
|
|||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
||||
|
||||
// it("point on left or right of line", () => {
|
||||
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
|
||||
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
|
||||
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
|
||||
|
||||
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
|
||||
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
|
||||
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
||||
// });
|
||||
|
||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||
|
||||
it("point on the line", () => {
|
||||
|
|
|
@ -12,7 +12,13 @@
|
|||
* to pure shapes
|
||||
*/
|
||||
|
||||
import type { Curve, LineSegment, Polygon, Radians } from "../../math";
|
||||
import type {
|
||||
Curve,
|
||||
LineSegment,
|
||||
Polygon,
|
||||
Radians,
|
||||
ViewportPoint,
|
||||
} from "../../math";
|
||||
import {
|
||||
curve,
|
||||
lineSegment,
|
||||
|
@ -56,24 +62,27 @@ import { invariant } from "../../excalidraw/utils";
|
|||
// a polyline (made up term here) is a line consisting of other line segments
|
||||
// this corresponds to a straight line element in the editor but it could also
|
||||
// be used to model other elements
|
||||
export type Polyline<Point extends GlobalPoint | LocalPoint> =
|
||||
export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||
LineSegment<Point>[];
|
||||
|
||||
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
|
||||
// curve on the canvas
|
||||
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
|
||||
export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||
Curve<Point>[];
|
||||
|
||||
// an ellipse is specified by its center, angle, and its major and minor axes
|
||||
// but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||
// in replace of semi major and semi minor axes
|
||||
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||
export type Ellipse<Point extends GlobalPoint | LocalPoint | ViewportPoint> = {
|
||||
center: Point;
|
||||
angle: Radians;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||
export type GeometricShape<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
> =
|
||||
| {
|
||||
type: "line";
|
||||
data: LineSegment<Point>;
|
||||
|
@ -239,7 +248,9 @@ export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
|||
};
|
||||
};
|
||||
|
||||
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
const polylineFromPoints = <
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
points: Point[],
|
||||
): Polyline<Point> => {
|
||||
let previousPoint: Point = points[0];
|
||||
|
@ -254,13 +265,15 @@ const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
|||
return polyline;
|
||||
};
|
||||
|
||||
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const getFreedrawShape = <
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
>(
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
center: Point,
|
||||
isClosed: boolean = false,
|
||||
): GeometricShape<Point> => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotateRads(
|
||||
const transform = (p: Point): Point =>
|
||||
pointRotateRads<Point>(
|
||||
pointFromVector(
|
||||
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
|
||||
),
|
||||
|
@ -391,7 +404,9 @@ export const segmentIntersectRectangleElement = <
|
|||
.filter((i): i is Point => !!i);
|
||||
};
|
||||
|
||||
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
const distanceToEllipse = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
|
@ -445,7 +460,9 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
|||
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
|
||||
};
|
||||
|
||||
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointOnEllipse = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
threshold = PRECISION,
|
||||
|
@ -453,7 +470,9 @@ export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
|||
return distanceToEllipse(point, ellipse) <= threshold;
|
||||
};
|
||||
|
||||
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
export const pointInEllipse = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue