mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
chore: Unify math types, utils and functions (#8389)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
e3d1dee9d0
commit
f4dd23fc31
98 changed files with 4291 additions and 3661 deletions
|
@ -1,7 +1,6 @@
|
||||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
import { type AppState } from "../../packages/excalidraw/types";
|
import { type AppState } from "../../packages/excalidraw/types";
|
||||||
import { throttleRAF } from "../../packages/excalidraw/utils";
|
import { throttleRAF } from "../../packages/excalidraw/utils";
|
||||||
import type { LineSegment } from "../../packages/utils";
|
|
||||||
import {
|
import {
|
||||||
bootstrapCanvas,
|
bootstrapCanvas,
|
||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
|
@ -13,12 +12,16 @@ import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
import { isLineSegment } from "../../packages/excalidraw/element/typeChecks";
|
import {
|
||||||
|
isLineSegment,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LineSegment,
|
||||||
|
} from "../../packages/math";
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
zoom: number,
|
zoom: number,
|
||||||
segment: LineSegment,
|
segment: LineSegment<GlobalPoint>,
|
||||||
color: string,
|
color: string,
|
||||||
) => {
|
) => {
|
||||||
context.save();
|
context.save();
|
||||||
|
@ -47,10 +50,15 @@ const render = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
frame.forEach((el) => {
|
frame.forEach((el: DebugElement) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isLineSegment(el.data):
|
case isLineSegment(el.data):
|
||||||
renderLine(context, appState.zoom.value, el.data, el.color);
|
renderLine(
|
||||||
|
context,
|
||||||
|
appState.zoom.value,
|
||||||
|
el.data as LineSegment<GlobalPoint>,
|
||||||
|
el.color,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/excalidraw",
|
||||||
"packages/utils",
|
"packages/utils",
|
||||||
|
"packages/math",
|
||||||
"examples/excalidraw",
|
"examples/excalidraw",
|
||||||
"examples/excalidraw/*"
|
"examples/excalidraw/*"
|
||||||
],
|
],
|
||||||
|
|
|
@ -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 } from "../math";
|
import { clamp } from "../../math";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
|
|
|
@ -42,20 +42,21 @@ export const actionDuplicateSelection = register({
|
||||||
perform: (elements, appState, formData, app) => {
|
perform: (elements, appState, formData, app) => {
|
||||||
// duplicate selected point(s) if editing a line
|
// duplicate selected point(s) if editing a line
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const ret = LinearElementEditor.duplicateSelectedPoints(
|
// TODO: Invariants should be checked here instead of duplicateSelectedPoints()
|
||||||
appState,
|
try {
|
||||||
app.scene.getNonDeletedElementsMap(),
|
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||||
);
|
appState,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!ret) {
|
return {
|
||||||
|
elements,
|
||||||
|
appState: newAppState,
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
elements,
|
|
||||||
appState: ret.appState,
|
|
||||||
storeAction: StoreAction.CAPTURE,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { done } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { isPathALoop } from "../math";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import {
|
||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
|
@ -16,6 +15,8 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { resetCursor } from "../cursor";
|
import { resetCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { point } from "../../math";
|
||||||
|
import { isPathALoop } from "../shapes";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
|
@ -112,10 +113,10 @@ export const actionFinalize = register({
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
mutateElement(multiPointElement, {
|
mutateElement(multiPointElement, {
|
||||||
points: linePoints.map((point, index) =>
|
points: linePoints.map((p, index) =>
|
||||||
index === linePoints.length - 1
|
index === linePoints.length - 1
|
||||||
? ([firstPoint[0], firstPoint[1]] as const)
|
? point(firstPoint[0], firstPoint[1])
|
||||||
: point,
|
: p,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { AppClassProperties, AppState, Point, Primitive } from "../types";
|
import type { AppClassProperties, AppState, Primitive } from "../types";
|
||||||
import type { StoreActionType } from "../store";
|
import type { StoreActionType } from "../store";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
|
@ -115,6 +115,8 @@ import {
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
|
import { point, vector } from "../../math";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -1648,10 +1650,10 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
[finalStartPoint, finalEndPoint].map(
|
[finalStartPoint, finalEndPoint].map(
|
||||||
(point) =>
|
(p): LocalPoint =>
|
||||||
[point[0] - newElement.x, point[1] - newElement.y] as Point,
|
point(p[0] - newElement.x, p[1] - newElement.y),
|
||||||
),
|
),
|
||||||
[0, 0],
|
vector(0, 0),
|
||||||
{
|
{
|
||||||
...(startElement && newElement.startBinding
|
...(startElement && newElement.startBinding
|
||||||
? {
|
? {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Radians } from "../math";
|
||||||
|
import { point } from "../math";
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
DEFAULT_CHART_COLOR_INDEX,
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
|
@ -203,7 +205,7 @@ const chartXLabels = (
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
angle: 5.87,
|
angle: 5.87 as Radians,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
|
@ -258,10 +260,7 @@ const chartLines = (
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
points: [
|
points: [point(0, 0), point(chartWidth, 0)],
|
||||||
[0, 0],
|
|
||||||
[chartWidth, 0],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
const yLine = newLinearElement({
|
||||||
|
@ -272,10 +271,7 @@ const chartLines = (
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
points: [
|
points: [point(0, 0), point(0, -chartHeight)],
|
||||||
[0, 0],
|
|
||||||
[0, -chartHeight],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
const maxLine = newLinearElement({
|
||||||
|
@ -288,10 +284,7 @@ const chartLines = (
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [
|
points: [point(0, 0), point(chartWidth, 0)],
|
||||||
[0, 0],
|
|
||||||
[chartWidth, 0],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
return [xLine, yLine, maxLine];
|
||||||
|
@ -448,10 +441,7 @@ const chartTypeLine = (
|
||||||
height: cy,
|
height: cy,
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
points: [
|
points: [point(0, 0), point(0, cy)],
|
||||||
[0, 0],
|
|
||||||
[0, cy],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -210,12 +210,6 @@ import {
|
||||||
isElementCompletelyInViewport,
|
isElementCompletelyInViewport,
|
||||||
isElementInViewport,
|
isElementInViewport,
|
||||||
} from "../element/sizeHelpers";
|
} from "../element/sizeHelpers";
|
||||||
import {
|
|
||||||
distance2d,
|
|
||||||
getCornerRadius,
|
|
||||||
getGridPoint,
|
|
||||||
isPathALoop,
|
|
||||||
} from "../math";
|
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
|
@ -230,7 +224,13 @@ import type {
|
||||||
ScrollBars,
|
ScrollBars,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { findShapeByKey, getBoundTextShape, getElementShape } from "../shapes";
|
import {
|
||||||
|
findShapeByKey,
|
||||||
|
getBoundTextShape,
|
||||||
|
getCornerRadius,
|
||||||
|
getElementShape,
|
||||||
|
isPathALoop,
|
||||||
|
} from "../shapes";
|
||||||
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
||||||
import { isPointInShape } from "../../utils/collision";
|
import { isPointInShape } from "../../utils/collision";
|
||||||
import type {
|
import type {
|
||||||
|
@ -386,6 +386,7 @@ import {
|
||||||
getReferenceSnapPoints,
|
getReferenceSnapPoints,
|
||||||
SnapCache,
|
SnapCache,
|
||||||
isGridModeEnabled,
|
isGridModeEnabled,
|
||||||
|
getGridPoint,
|
||||||
} from "../snapping";
|
} from "../snapping";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
|
@ -439,6 +440,8 @@ import {
|
||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
getLinkDirectionFromKey,
|
getLinkDirectionFromKey,
|
||||||
} from "../element/flowchart";
|
} from "../element/flowchart";
|
||||||
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
|
import { point, pointDistance, vector } from "../../math";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
|
@ -4844,7 +4847,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.getElementHitThreshold(),
|
this.getElementHitThreshold(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isPointInShape([x, y], selectionShape);
|
return isPointInShape(point(x, y), selectionShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
// take bound text element into consideration for hit collision as well
|
// take bound text element into consideration for hit collision as well
|
||||||
|
@ -5035,7 +5038,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
lineHeight,
|
lineHeight,
|
||||||
angle: container?.angle ?? 0,
|
angle: container?.angle ?? (0 as Radians),
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5203,7 +5206,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
[scenePointer.x, scenePointer.y],
|
point(scenePointer.x, scenePointer.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -5214,11 +5217,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
isTouchScreen: boolean,
|
isTouchScreen: boolean,
|
||||||
) => {
|
) => {
|
||||||
const draggedDistance = distance2d(
|
const draggedDistance = pointDistance(
|
||||||
this.lastPointerDownEvent!.clientX,
|
point(
|
||||||
this.lastPointerDownEvent!.clientY,
|
this.lastPointerDownEvent!.clientX,
|
||||||
this.lastPointerUpEvent!.clientX,
|
this.lastPointerDownEvent!.clientY,
|
||||||
this.lastPointerUpEvent!.clientY,
|
),
|
||||||
|
point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!this.hitLinkElement ||
|
!this.hitLinkElement ||
|
||||||
|
@ -5237,7 +5241,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
point(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
|
@ -5248,7 +5252,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
point(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||||
this.device.editor.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
|
@ -5497,17 +5501,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// if we haven't yet created a temp point and we're beyond commit-zone
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
||||||
// threshold, add a point
|
// threshold, add a point
|
||||||
if (
|
if (
|
||||||
distance2d(
|
pointDistance(
|
||||||
scenePointerX - rx,
|
point(scenePointerX - rx, scenePointerY - ry),
|
||||||
scenePointerY - ry,
|
lastPoint,
|
||||||
lastPoint[0],
|
|
||||||
lastPoint[1],
|
|
||||||
) >= LINE_CONFIRM_THRESHOLD
|
) >= LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
{
|
{
|
||||||
points: [...points, [scenePointerX - rx, scenePointerY - ry]],
|
points: [
|
||||||
|
...points,
|
||||||
|
point<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -5519,11 +5524,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
} else if (
|
} else if (
|
||||||
points.length > 2 &&
|
points.length > 2 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
distance2d(
|
pointDistance(
|
||||||
scenePointerX - rx,
|
point(scenePointerX - rx, scenePointerY - ry),
|
||||||
scenePointerY - ry,
|
lastCommittedPoint,
|
||||||
lastCommittedPoint[0],
|
|
||||||
lastCommittedPoint[1],
|
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
@ -5570,10 +5573,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
[
|
[
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
[
|
point<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -5589,10 +5592,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
...points.slice(0, -1),
|
...points.slice(0, -1),
|
||||||
[
|
point<LocalPoint>(
|
||||||
lastCommittedX + dxFromLastCommitted,
|
lastCommittedX + dxFromLastCommitted,
|
||||||
lastCommittedY + dyFromLastCommitted,
|
lastCommittedY + dyFromLastCommitted,
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
@ -5817,17 +5820,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const distance = distance2d(
|
const distance = pointDistance(
|
||||||
pointerDownState.lastCoords.x,
|
point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
|
||||||
pointerDownState.lastCoords.y,
|
point(scenePointer.x, scenePointer.y),
|
||||||
scenePointer.x,
|
|
||||||
scenePointer.y,
|
|
||||||
);
|
);
|
||||||
const threshold = this.getElementHitThreshold();
|
const threshold = this.getElementHitThreshold();
|
||||||
const point = { ...pointerDownState.lastCoords };
|
const p = { ...pointerDownState.lastCoords };
|
||||||
let samplingInterval = 0;
|
let samplingInterval = 0;
|
||||||
while (samplingInterval <= distance) {
|
while (samplingInterval <= distance) {
|
||||||
const hitElements = this.getElementsAtPosition(point.x, point.y);
|
const hitElements = this.getElementsAtPosition(p.x, p.y);
|
||||||
processElements(hitElements);
|
processElements(hitElements);
|
||||||
|
|
||||||
// Exit since we reached current point
|
// Exit since we reached current point
|
||||||
|
@ -5839,12 +5840,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
||||||
|
|
||||||
const distanceRatio = samplingInterval / distance;
|
const distanceRatio = samplingInterval / distance;
|
||||||
const nextX =
|
const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x;
|
||||||
(1 - distanceRatio) * point.x + distanceRatio * scenePointer.x;
|
const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y;
|
||||||
const nextY =
|
p.x = nextX;
|
||||||
(1 - distanceRatio) * point.y + distanceRatio * scenePointer.y;
|
p.y = nextY;
|
||||||
point.x = nextX;
|
|
||||||
point.y = nextY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pointerDownState.lastCoords.x = scenePointer.x;
|
pointerDownState.lastCoords.x = scenePointer.x;
|
||||||
|
@ -6325,7 +6324,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
[scenePointer.x, scenePointer.y],
|
point(scenePointer.x, scenePointer.y),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||||
|
@ -7008,7 +7007,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
simulatePressure,
|
simulatePressure,
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
points: [[0, 0]],
|
points: [point<LocalPoint>(0, 0)],
|
||||||
pressures: simulatePressure ? [] : [event.pressure],
|
pressures: simulatePressure ? [] : [event.pressure],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7216,11 +7215,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (
|
if (
|
||||||
multiElement.points.length > 1 &&
|
multiElement.points.length > 1 &&
|
||||||
lastCommittedPoint &&
|
lastCommittedPoint &&
|
||||||
distance2d(
|
pointDistance(
|
||||||
pointerDownState.origin.x - rx,
|
point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry),
|
||||||
pointerDownState.origin.y - ry,
|
lastCommittedPoint,
|
||||||
lastCommittedPoint[0],
|
|
||||||
lastCommittedPoint[1],
|
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
@ -7321,7 +7318,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [...element.points, [0, 0]],
|
points: [...element.points, point<LocalPoint>(0, 0)],
|
||||||
});
|
});
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
|
@ -7573,11 +7570,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.activeTool.type === "line")
|
this.state.activeTool.type === "line")
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
distance2d(
|
pointDistance(
|
||||||
pointerCoords.x,
|
point(pointerCoords.x, pointerCoords.y),
|
||||||
pointerCoords.y,
|
point(pointerDownState.origin.x, pointerDownState.origin.y),
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
) < DRAGGING_THRESHOLD
|
) < DRAGGING_THRESHOLD
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
@ -7926,7 +7921,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, [dx, dy]],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
@ -7955,7 +7950,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, [dx, dy]],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -7963,8 +7958,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
mutateElbowArrow(
|
mutateElbowArrow(
|
||||||
newElement,
|
newElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
[...points.slice(0, -1), [dx, dy]],
|
[...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||||
[0, 0],
|
vector(0, 0),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
|
@ -7975,7 +7970,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points.slice(0, -1), [dx, dy]],
|
points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -8284,9 +8279,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
: [...newElement.pressures, childEvent.pressure];
|
: [...newElement.pressures, childEvent.pressure];
|
||||||
|
|
||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [...points, [dx, dy]],
|
points: [...points, point<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
lastCommittedPoint: [dx, dy],
|
lastCommittedPoint: point<LocalPoint>(dx, dy),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
@ -8333,7 +8328,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
points: [
|
points: [
|
||||||
...newElement.points,
|
...newElement.points,
|
||||||
[pointerCoords.x - newElement.x, pointerCoords.y - newElement.y],
|
point<LocalPoint>(
|
||||||
|
pointerCoords.x - newElement.x,
|
||||||
|
pointerCoords.y - newElement.y,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -8643,11 +8641,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (isEraserActive(this.state) && pointerStart && pointerEnd) {
|
if (isEraserActive(this.state) && pointerStart && pointerEnd) {
|
||||||
this.eraserTrail.endPath();
|
this.eraserTrail.endPath();
|
||||||
|
|
||||||
const draggedDistance = distance2d(
|
const draggedDistance = pointDistance(
|
||||||
pointerStart.clientX,
|
point(pointerStart.clientX, pointerStart.clientY),
|
||||||
pointerStart.clientY,
|
point(pointerEnd.clientX, pointerEnd.clientY),
|
||||||
pointerEnd.clientX,
|
|
||||||
pointerEnd.clientY,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (draggedDistance === 0) {
|
if (draggedDistance === 0) {
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { mutateElement } from "../../element/mutateElement";
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
|
import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import type { Degrees } from "../../../math";
|
||||||
|
import { degreesToRadians, radiansToDegrees } from "../../../math";
|
||||||
|
|
||||||
interface AngleProps {
|
interface AngleProps {
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
|
@ -36,7 +37,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
|
@ -51,7 +52,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalAngleInDegrees =
|
const originalAngleInDegrees =
|
||||||
Math.round(radianToDegree(origElement.angle) * 100) / 100;
|
Math.round(radiansToDegrees(origElement.angle) * 100) / 100;
|
||||||
const changeInDegrees = Math.round(accumulatedChange);
|
const changeInDegrees = Math.round(accumulatedChange);
|
||||||
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
|
@ -61,7 +62,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
nextAngleInDegrees =
|
nextAngleInDegrees =
|
||||||
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
||||||
|
|
||||||
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
|
@ -80,7 +81,7 @@ const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
||||||
<DragInput
|
<DragInput
|
||||||
label="A"
|
label="A"
|
||||||
icon={angleIcon}
|
icon={angleIcon}
|
||||||
value={Math.round((radianToDegree(element.angle) % 360) * 100) / 100}
|
value={Math.round((radiansToDegrees(element.angle) % 360) * 100) / 100}
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handleDegreeChange}
|
dragInputCallback={handleDegreeChange}
|
||||||
editable={isPropertyEditable(element, "angle")}
|
editable={isPropertyEditable(element, "angle")}
|
||||||
|
|
|
@ -3,13 +3,14 @@ import { getBoundTextElement } from "../../element/textElement";
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
import { isArrowElement } from "../../element/typeChecks";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import type { Degrees } from "../../../math";
|
||||||
|
import { degreesToRadians, radiansToDegrees } from "../../../math";
|
||||||
|
|
||||||
interface MultiAngleProps {
|
interface MultiAngleProps {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
@ -39,7 +40,7 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||||
|
|
||||||
for (const element of editableLatestIndividualElements) {
|
for (const element of editableLatestIndividualElements) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
@ -71,7 +72,7 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
}
|
}
|
||||||
const originalElement = editableOriginalIndividualElements[i];
|
const originalElement = editableOriginalIndividualElements[i];
|
||||||
const originalAngleInDegrees =
|
const originalAngleInDegrees =
|
||||||
Math.round(radianToDegree(originalElement.angle) * 100) / 100;
|
Math.round(radiansToDegrees(originalElement.angle) * 100) / 100;
|
||||||
const changeInDegrees = Math.round(accumulatedChange);
|
const changeInDegrees = Math.round(accumulatedChange);
|
||||||
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
|
@ -81,7 +82,7 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
nextAngleInDegrees =
|
nextAngleInDegrees =
|
||||||
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
||||||
|
|
||||||
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
latestElement,
|
latestElement,
|
||||||
|
@ -109,7 +110,7 @@ const MultiAngle = ({
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
||||||
);
|
);
|
||||||
const angles = editableLatestIndividualElements.map(
|
const angles = editableLatestIndividualElements.map(
|
||||||
(el) => Math.round((radianToDegree(el.angle) % 360) * 100) / 100,
|
(el) => Math.round((radiansToDegrees(el.angle) % 360) * 100) / 100,
|
||||||
);
|
);
|
||||||
const value = new Set(angles).size === 1 ? angles[0] : "Mixed";
|
const value = new Set(angles).size === 1 ? angles[0] : "Mixed";
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,14 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState, Point } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
|
import { point, type GlobalPoint } from "../../../math";
|
||||||
|
|
||||||
interface MultiDimensionProps {
|
interface MultiDimensionProps {
|
||||||
property: "width" | "height";
|
property: "width" | "height";
|
||||||
|
@ -104,7 +105,7 @@ const resizeGroup = (
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
initialHeight: number,
|
initialHeight: number,
|
||||||
aspectRatio: number,
|
aspectRatio: number,
|
||||||
anchor: Point,
|
anchor: GlobalPoint,
|
||||||
property: MultiDimensionProps["property"],
|
property: MultiDimensionProps["property"],
|
||||||
latestElements: ExcalidrawElement[],
|
latestElements: ExcalidrawElement[],
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
|
@ -181,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
initialHeight,
|
initialHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
[x1, y1],
|
point(x1, y1),
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
@ -286,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
initialHeight,
|
initialHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
[x1, y1],
|
point(x1, y1),
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { rotate } from "../../math";
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
|
@ -14,6 +13,7 @@ import { useMemo } from "react";
|
||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import { point, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
interface MultiPositionProps {
|
interface MultiPositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
|
@ -43,11 +43,9 @@ const moveElements = (
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
origElement.x,
|
point(origElement.x, origElement.y),
|
||||||
origElement.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -98,11 +96,9 @@ const moveGroupTo = (
|
||||||
latestElement.y + latestElement.height / 2,
|
latestElement.y + latestElement.height / 2,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
latestElement.x,
|
point(latestElement.x, latestElement.y),
|
||||||
latestElement.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
latestElement.angle,
|
latestElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -174,11 +170,9 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
origElement.x,
|
point(origElement.x, origElement.y),
|
||||||
origElement.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -246,7 +240,11 @@ const MultiPosition = ({
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = rotate(el.x, el.y, cx, cy, el.angle);
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
|
point(el.x, el.y),
|
||||||
|
point(cx, cy),
|
||||||
|
el.angle,
|
||||||
|
);
|
||||||
|
|
||||||
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
||||||
import { rotate } from "../../math";
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import { point, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
|
@ -32,11 +32,9 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
origElement.y + origElement.height / 2,
|
origElement.y + origElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
origElement.x,
|
point(origElement.x, origElement.y),
|
||||||
origElement.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
origElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,11 +92,9 @@ const Position = ({
|
||||||
scene,
|
scene,
|
||||||
appState,
|
appState,
|
||||||
}: PositionProps) => {
|
}: PositionProps) => {
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
element.x,
|
point(element.x, element.y),
|
||||||
element.y,
|
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const value =
|
const value =
|
||||||
|
|
|
@ -19,12 +19,13 @@ import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { degreeToRadian, rotate } from "../../math";
|
|
||||||
import { getTextEditor, updateTextEditor } from "../../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../../tests/queries/dom";
|
||||||
import { getCommonBounds, isTextElement } from "../../element";
|
import { getCommonBounds, isTextElement } from "../../element";
|
||||||
import { API } from "../../tests/helpers/api";
|
import { API } from "../../tests/helpers/api";
|
||||||
import { actionGroup } from "../../actions";
|
import { actionGroup } from "../../actions";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
|
import type { Degrees } from "../../../math";
|
||||||
|
import { degreesToRadians, point, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
@ -46,7 +47,9 @@ const testInputProperty = (
|
||||||
expect(input.value).toBe(initialValue.toString());
|
expect(input.value).toBe(initialValue.toString());
|
||||||
UI.updateInput(input, String(nextValue));
|
UI.updateInput(input, String(nextValue));
|
||||||
if (property === "angle") {
|
if (property === "angle") {
|
||||||
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
expect(element[property]).toBe(
|
||||||
|
degreesToRadians(Number(nextValue) as Degrees),
|
||||||
|
);
|
||||||
} else if (property === "fontSize" && isTextElement(element)) {
|
} else if (property === "fontSize" && isTextElement(element)) {
|
||||||
expect(element[property]).toBe(Number(nextValue));
|
expect(element[property]).toBe(Number(nextValue));
|
||||||
} else if (property !== "fontSize") {
|
} else if (property !== "fontSize") {
|
||||||
|
@ -260,11 +263,9 @@ describe("stats for a generic element", () => {
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -281,11 +282,9 @@ describe("stats for a generic element", () => {
|
||||||
|
|
||||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
testInputProperty(rectangle, "angle", "A", 0, 45);
|
||||||
|
|
||||||
let [newTopLeftX, newTopLeftY] = rotate(
|
let [newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -294,11 +293,9 @@ describe("stats for a generic element", () => {
|
||||||
|
|
||||||
testInputProperty(rectangle, "angle", "A", 45, 66);
|
testInputProperty(rectangle, "angle", "A", 45, 66);
|
||||||
|
|
||||||
[newTopLeftX, newTopLeftY] = rotate(
|
[newTopLeftX, newTopLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
||||||
|
@ -313,11 +310,9 @@ describe("stats for a generic element", () => {
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
||||||
|
@ -325,11 +320,9 @@ describe("stats for a generic element", () => {
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
let [currentTopLeftX, currentTopLeftY] = rotate(
|
let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
||||||
|
@ -340,11 +333,9 @@ describe("stats for a generic element", () => {
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
];
|
];
|
||||||
[currentTopLeftX, currentTopLeftY] = rotate(
|
[currentTopLeftX, currentTopLeftY] = pointRotateRads(
|
||||||
rectangle.x,
|
point(rectangle.x, rectangle.y),
|
||||||
rectangle.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
rectangle.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -642,7 +633,7 @@ describe("stats for multiple elements", () => {
|
||||||
|
|
||||||
UI.updateInput(angle, "40");
|
UI.updateInput(angle, "40");
|
||||||
|
|
||||||
const angleInRadian = degreeToRadian(40);
|
const angleInRadian = degreesToRadians(40 as Degrees);
|
||||||
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
||||||
expect(text?.angle).toBeCloseTo(angleInRadian, 4);
|
expect(text?.angle).toBeCloseTo(angleInRadian, 4);
|
||||||
expect(frame.angle).toBe(0);
|
expect(frame.angle).toBe(0);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Radians } from "../../../math";
|
||||||
|
import { point, pointRotateRads } from "../../../math";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
|
@ -30,7 +32,6 @@ import {
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
isInGroup,
|
isInGroup,
|
||||||
} from "../../groups";
|
} from "../../groups";
|
||||||
import { rotate } from "../../math";
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { getFontString } from "../../utils";
|
import { getFontString } from "../../utils";
|
||||||
|
@ -229,23 +230,19 @@ export const moveElement = (
|
||||||
originalElement.x + originalElement.width / 2,
|
originalElement.x + originalElement.width / 2,
|
||||||
originalElement.y + originalElement.height / 2,
|
originalElement.y + originalElement.height / 2,
|
||||||
];
|
];
|
||||||
const [topLeftX, topLeftY] = rotate(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
originalElement.x,
|
point(originalElement.x, originalElement.y),
|
||||||
originalElement.y,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
originalElement.angle,
|
originalElement.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeInX = newTopLeftX - topLeftX;
|
const changeInX = newTopLeftX - topLeftX;
|
||||||
const changeInY = newTopLeftY - topLeftY;
|
const changeInY = newTopLeftY - topLeftY;
|
||||||
|
|
||||||
const [x, y] = rotate(
|
const [x, y] = pointRotateRads(
|
||||||
newTopLeftX,
|
point(newTopLeftX, newTopLeftY),
|
||||||
newTopLeftY,
|
point(cx + changeInX, cy + changeInY),
|
||||||
cx + changeInX,
|
-originalElement.angle as Radians,
|
||||||
cy + changeInY,
|
|
||||||
-originalElement.angle,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
|
|
|
@ -25,11 +25,11 @@ import type { BinaryFiles } from "../../types";
|
||||||
import { ArrowRightIcon } from "../icons";
|
import { ArrowRightIcon } from "../icons";
|
||||||
|
|
||||||
import "./TTDDialog.scss";
|
import "./TTDDialog.scss";
|
||||||
import { isFiniteNumber } from "../../utils";
|
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut";
|
import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut";
|
||||||
|
import { isFiniteNumber } from "../../../math";
|
||||||
|
|
||||||
const MIN_PROMPT_LENGTH = 3;
|
const MIN_PROMPT_LENGTH = 3;
|
||||||
const MAX_PROMPT_LENGTH = 1000;
|
const MAX_PROMPT_LENGTH = 1000;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AppState, ExcalidrawProps, Point, UIAppState } from "../../types";
|
import type { AppState, ExcalidrawProps, UIAppState } from "../../types";
|
||||||
import {
|
import {
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
|
@ -36,6 +36,7 @@ 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";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
|
@ -176,10 +177,12 @@ export const Hyperlink = ({
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
const shouldHide = shouldHideLinkPopup(element, elementsMap, appState, [
|
const shouldHide = shouldHideLinkPopup(
|
||||||
event.clientX,
|
element,
|
||||||
event.clientY,
|
elementsMap,
|
||||||
]) as boolean;
|
appState,
|
||||||
|
point(event.clientX, event.clientY),
|
||||||
|
) as boolean;
|
||||||
if (shouldHide) {
|
if (shouldHide) {
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
setAppState({ showHyperlinkPopup: false });
|
setAppState({ showHyperlinkPopup: false });
|
||||||
|
@ -416,7 +419,7 @@ const shouldHideLinkPopup = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
[clientX, clientY]: Point,
|
[clientX, clientY]: GlobalPoint,
|
||||||
): Boolean => {
|
): Boolean => {
|
||||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
{ clientX, clientY },
|
{ clientX, clientY },
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { GlobalPoint, Radians } from "../../../math";
|
||||||
|
import { point, pointRotateRads } from "../../../math";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import type { Bounds } from "../../element/bounds";
|
import type { Bounds } from "../../element/bounds";
|
||||||
import { getElementAbsoluteCoords } from "../../element/bounds";
|
import { getElementAbsoluteCoords } from "../../element/bounds";
|
||||||
|
@ -6,9 +8,8 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { rotate } from "../../math";
|
|
||||||
import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
|
import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
|
||||||
import type { AppState, Point, UIAppState } from "../../types";
|
import type { AppState, UIAppState } from "../../types";
|
||||||
|
|
||||||
export const EXTERNAL_LINK_IMG = document.createElement("img");
|
export const EXTERNAL_LINK_IMG = document.createElement("img");
|
||||||
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||||
|
@ -17,7 +18,7 @@ EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||||
|
|
||||||
export const getLinkHandleFromCoords = (
|
export const getLinkHandleFromCoords = (
|
||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: Radians,
|
||||||
appState: Pick<UIAppState, "zoom">,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
|
@ -33,11 +34,9 @@ export const getLinkHandleFromCoords = (
|
||||||
const x = x2 + dashedLineMargin - centeringOffset;
|
const x = x2 + dashedLineMargin - centeringOffset;
|
||||||
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
||||||
|
|
||||||
const [rotatedX, rotatedY] = rotate(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
x + linkWidth / 2,
|
point(x + linkWidth / 2, y + linkHeight / 2),
|
||||||
y + linkHeight / 2,
|
point(centerX, centerY),
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
|
@ -52,7 +51,7 @@ export const isPointHittingLinkIcon = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
[x, y]: Point,
|
[x, y]: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const threshold = 4 / appState.zoom.value;
|
const threshold = 4 / appState.zoom.value;
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
@ -73,7 +72,7 @@ export const isPointHittingLink = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
[x, y]: Point,
|
[x, y]: GlobalPoint,
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!element.link || appState.selectedElementIds[element.id]) {
|
if (!element.link || appState.selectedElementIds[element.id]) {
|
||||||
|
@ -86,5 +85,5 @@ export const isPointHittingLink = (
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]);
|
return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y));
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,11 +40,7 @@ import {
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { bumpVersion } from "../element/mutateElement";
|
import { bumpVersion } from "../element/mutateElement";
|
||||||
import {
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
getUpdatedTimestamp,
|
|
||||||
isFiniteNumber,
|
|
||||||
updateActiveTool,
|
|
||||||
} from "../utils";
|
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { MarkOptional, Mutable } from "../utility-types";
|
import type { MarkOptional, Mutable } from "../utility-types";
|
||||||
import { detectLineHeight, getContainerElement } from "../element/textElement";
|
import { detectLineHeight, getContainerElement } from "../element/textElement";
|
||||||
|
@ -58,6 +54,8 @@ import {
|
||||||
getNormalizedGridStep,
|
getNormalizedGridStep,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
|
import { isFiniteNumber, point } from "../../math";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -152,7 +150,7 @@ const restoreElementWithProperties = <
|
||||||
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
opacity:
|
opacity:
|
||||||
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
||||||
angle: element.angle || 0,
|
angle: element.angle || (0 as Radians),
|
||||||
x: extra.x ?? element.x ?? 0,
|
x: extra.x ?? element.x ?? 0,
|
||||||
y: extra.y ?? element.y ?? 0,
|
y: extra.y ?? element.y ?? 0,
|
||||||
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
|
@ -266,10 +264,7 @@ const restoreElement = (
|
||||||
let y = element.y;
|
let y = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points = // migrate old arrow model to new one
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
? [
|
? [point(0, 0), point(element.width, element.height)]
|
||||||
[0, 0],
|
|
||||||
[element.width, element.height],
|
|
||||||
]
|
|
||||||
: element.points;
|
: element.points;
|
||||||
|
|
||||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
|
@ -293,14 +288,11 @@ const restoreElement = (
|
||||||
});
|
});
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
||||||
let x = element.x;
|
let x: number | undefined = element.x;
|
||||||
let y = element.y;
|
let y: number | undefined = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
? [
|
? [point(0, 0), point(element.width, element.height)]
|
||||||
[0, 0],
|
|
||||||
[element.width, element.height],
|
|
||||||
]
|
|
||||||
: element.points;
|
: element.points;
|
||||||
|
|
||||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { vi } from "vitest";
|
||||||
import type { ExcalidrawElementSkeleton } from "./transform";
|
import type { ExcalidrawElementSkeleton } from "./transform";
|
||||||
import { convertToExcalidrawElements } from "./transform";
|
import { convertToExcalidrawElements } from "./transform";
|
||||||
import type { ExcalidrawArrowElement } from "../element/types";
|
import type { ExcalidrawArrowElement } from "../element/types";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
const opts = { regenerateIds: false };
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
|
@ -911,10 +912,7 @@ describe("Test Transform", () => {
|
||||||
x: 111.262,
|
x: 111.262,
|
||||||
y: 57,
|
y: 57,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [
|
points: [point(0, 0), point(272.985, 0)],
|
||||||
[0, 0],
|
|
||||||
[272.985, 0],
|
|
||||||
],
|
|
||||||
label: {
|
label: {
|
||||||
text: "How are you?",
|
text: "How are you?",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
@ -937,7 +935,7 @@ describe("Test Transform", () => {
|
||||||
x: 77.017,
|
x: 77.017,
|
||||||
y: 79,
|
y: 79,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: [[0, 0]],
|
points: [point(0, 0)],
|
||||||
label: {
|
label: {
|
||||||
text: "Friendship",
|
text: "Friendship",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|
|
@ -53,6 +53,7 @@ import { randomId } from "../random";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
import { isArrowElement } from "../element/typeChecks";
|
import { isArrowElement } from "../element/typeChecks";
|
||||||
|
import { point, type LocalPoint } from "../../math";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
|
@ -417,7 +418,7 @@ const bindLinearElementToElement = (
|
||||||
const endPointIndex = linearElement.points.length - 1;
|
const endPointIndex = linearElement.points.length - 1;
|
||||||
const delta = 0.5;
|
const delta = 0.5;
|
||||||
|
|
||||||
const newPoints = cloneJSON(linearElement.points) as [number, number][];
|
const newPoints = cloneJSON<readonly LocalPoint[]>(linearElement.points);
|
||||||
|
|
||||||
// left to right so shift the arrow towards right
|
// left to right so shift the arrow towards right
|
||||||
if (
|
if (
|
||||||
|
@ -535,10 +536,7 @@ export const convertToExcalidrawElements = (
|
||||||
excalidrawElement = newLinearElement({
|
excalidrawElement = newLinearElement({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
points: [
|
points: [point(0, 0), point(width, height)],
|
||||||
[0, 0],
|
|
||||||
[width, height],
|
|
||||||
],
|
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -551,10 +549,7 @@ export const convertToExcalidrawElements = (
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
points: [
|
points: [point(0, 0), point(width, height)],
|
||||||
[0, 0],
|
|
||||||
[width, height],
|
|
||||||
],
|
|
||||||
...element,
|
...element,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as GA from "../ga";
|
import * as GA from "../../math/ga/ga";
|
||||||
import * as GAPoint from "../gapoints";
|
import * as GAPoint from "../../math/ga/gapoints";
|
||||||
import * as GADirection from "../gadirections";
|
import * as GADirection from "../../math/ga/gadirections";
|
||||||
import * as GALine from "../galines";
|
import * as GALine from "../../math/ga/galines";
|
||||||
import * as GATransform from "../gatransforms";
|
import * as GATransform from "../../math/ga/gatransforms";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
@ -10,7 +10,6 @@ import type {
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawFreeDrawElement,
|
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
|
@ -26,11 +25,12 @@ import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
import type { AppState, Point } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { isPointOnShape } from "../../utils/collision";
|
import { isPointOnShape } from "../../utils/collision";
|
||||||
import { getElementAtPosition } from "../scene";
|
import { getElementAtPosition } from "../scene";
|
||||||
import {
|
import {
|
||||||
|
@ -51,17 +51,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { arrayToMap, tupleToCoors } from "../utils";
|
import { arrayToMap, tupleToCoors } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getElementShape } from "../shapes";
|
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
||||||
import {
|
|
||||||
aabbForElement,
|
|
||||||
clamp,
|
|
||||||
distanceSq2d,
|
|
||||||
getCenterForBounds,
|
|
||||||
getCenterForElement,
|
|
||||||
pointInsideBounds,
|
|
||||||
pointToVector,
|
|
||||||
rotatePoint,
|
|
||||||
} from "../math";
|
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
compareHeading,
|
||||||
HEADING_DOWN,
|
HEADING_DOWN,
|
||||||
|
@ -72,7 +62,18 @@ import {
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
type Heading,
|
type Heading,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
import { segmentIntersectRectangleElement } from "../../utils/geometry/geometry";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
|
import {
|
||||||
|
lineSegment,
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
type GlobalPoint,
|
||||||
|
vectorFromPoint,
|
||||||
|
pointFromPair,
|
||||||
|
pointDistanceSq,
|
||||||
|
clamp,
|
||||||
|
} from "../../math";
|
||||||
|
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
|
@ -649,7 +650,7 @@ export const updateBoundElements = (
|
||||||
update,
|
update,
|
||||||
): update is NonNullable<{
|
): update is NonNullable<{
|
||||||
index: number;
|
index: number;
|
||||||
point: Point;
|
point: LocalPoint;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
}> => update !== null,
|
}> => update !== null,
|
||||||
);
|
);
|
||||||
|
@ -695,14 +696,14 @@ const getSimultaneouslyUpdatedElementIds = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHeadingForElbowArrowSnap = (
|
export const getHeadingForElbowArrowSnap = (
|
||||||
point: Readonly<Point>,
|
p: Readonly<GlobalPoint>,
|
||||||
otherPoint: Readonly<Point>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
origPoint: Point,
|
origPoint: GlobalPoint,
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const otherPointHeading = vectorToHeading(pointToVector(otherPoint, point));
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
||||||
|
|
||||||
if (!bindableElement || !aabb) {
|
if (!bindableElement || !aabb) {
|
||||||
return otherPointHeading;
|
return otherPointHeading;
|
||||||
|
@ -716,17 +717,23 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
pointToVector(point, getCenterForElement(bindableElement)),
|
vectorFromPoint(
|
||||||
|
p,
|
||||||
|
point<GlobalPoint>(
|
||||||
|
bindableElement.x + bindableElement.width / 2,
|
||||||
|
bindableElement.y + bindableElement.height / 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointHeading = headingForPointFromElement(bindableElement, aabb, point);
|
const pointHeading = headingForPointFromElement(bindableElement, aabb, p);
|
||||||
|
|
||||||
return pointHeading;
|
return pointHeading;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDistanceForBinding = (
|
const getDistanceForBinding = (
|
||||||
point: Readonly<Point>,
|
point: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
|
@ -745,89 +752,87 @@ const getDistanceForBinding = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindPointToSnapToElementOutline = (
|
export const bindPointToSnapToElementOutline = (
|
||||||
point: Readonly<Point>,
|
p: Readonly<GlobalPoint>,
|
||||||
otherPoint: Readonly<Point>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined,
|
bindableElement: ExcalidrawBindableElement | undefined,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
const aabb = bindableElement && aabbForElement(bindableElement);
|
const aabb = bindableElement && aabbForElement(bindableElement);
|
||||||
|
|
||||||
if (bindableElement && aabb) {
|
if (bindableElement && aabb) {
|
||||||
// TODO: Dirty hacks until tangents are properly calculated
|
// TODO: Dirty hacks until tangents are properly calculated
|
||||||
const heading = headingForPointFromElement(bindableElement, aabb, point);
|
const heading = headingForPointFromElement(bindableElement, aabb, p);
|
||||||
const intersections = [
|
const intersections = [
|
||||||
...intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
[point[0], point[1] - 2 * bindableElement.height],
|
point(p[0], p[1] - 2 * bindableElement.height),
|
||||||
[point[0], point[1] + 2 * bindableElement.height],
|
point(p[0], p[1] + 2 * bindableElement.height),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
) ?? []),
|
||||||
...intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
[point[0] - 2 * bindableElement.width, point[1]],
|
point(p[0] - 2 * bindableElement.width, p[1]),
|
||||||
[point[0] + 2 * bindableElement.width, point[1]],
|
point(p[0] + 2 * bindableElement.width, p[1]),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
) ?? []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const isVertical =
|
const isVertical =
|
||||||
compareHeading(heading, HEADING_LEFT) ||
|
compareHeading(heading, HEADING_LEFT) ||
|
||||||
compareHeading(heading, HEADING_RIGHT);
|
compareHeading(heading, HEADING_RIGHT);
|
||||||
const dist = Math.abs(
|
const dist = Math.abs(
|
||||||
distanceToBindableElement(bindableElement, point, elementsMap),
|
distanceToBindableElement(bindableElement, p, elementsMap),
|
||||||
);
|
);
|
||||||
const isInner = isVertical
|
const isInner = isVertical
|
||||||
? dist < bindableElement.width * -0.1
|
? dist < bindableElement.width * -0.1
|
||||||
: dist < bindableElement.height * -0.1;
|
: dist < bindableElement.height * -0.1;
|
||||||
|
|
||||||
intersections.sort(
|
intersections.sort((a, b) => pointDistanceSq(a, p) - pointDistanceSq(b, p));
|
||||||
(a, b) => distanceSq2d(a, point) - distanceSq2d(b, point),
|
|
||||||
);
|
|
||||||
|
|
||||||
return isInner
|
return isInner
|
||||||
? headingToMidBindPoint(otherPoint, bindableElement, aabb)
|
? headingToMidBindPoint(otherPoint, bindableElement, aabb)
|
||||||
: intersections.filter((i) =>
|
: intersections.filter((i) =>
|
||||||
isVertical
|
isVertical
|
||||||
? Math.abs(point[1] - i[1]) < 0.1
|
? Math.abs(p[1] - i[1]) < 0.1
|
||||||
: Math.abs(point[0] - i[0]) < 0.1,
|
: Math.abs(p[0] - i[0]) < 0.1,
|
||||||
)[0] ?? point;
|
)[0] ?? point;
|
||||||
}
|
}
|
||||||
|
|
||||||
return point;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
const headingToMidBindPoint = (
|
const headingToMidBindPoint = (
|
||||||
point: Point,
|
p: GlobalPoint,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
aabb: Bounds,
|
aabb: Bounds,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
const center = getCenterForBounds(aabb);
|
const center = getCenterForBounds(aabb);
|
||||||
const heading = vectorToHeading(pointToVector(point, center));
|
const heading = vectorToHeading(vectorFromPoint(p, center));
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case compareHeading(heading, HEADING_UP):
|
case compareHeading(heading, HEADING_UP):
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[(aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]],
|
point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
case compareHeading(heading, HEADING_RIGHT):
|
case compareHeading(heading, HEADING_RIGHT):
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1],
|
point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
case compareHeading(heading, HEADING_DOWN):
|
case compareHeading(heading, HEADING_DOWN):
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[(aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]],
|
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1],
|
point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||||
center,
|
center,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
);
|
);
|
||||||
|
@ -836,22 +841,25 @@ const headingToMidBindPoint = (
|
||||||
|
|
||||||
export const avoidRectangularCorner = (
|
export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: Point,
|
p: GlobalPoint,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
const center = getCenterForElement(element);
|
const center = point<GlobalPoint>(
|
||||||
const nonRotatedPoint = rotatePoint(p, center, -element.angle);
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
// Top left
|
// Top left
|
||||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
||||||
return rotatePoint(
|
return pointRotateRads<GlobalPoint>(
|
||||||
[element.x - FIXED_BINDING_DISTANCE, element.y],
|
point(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x, element.y - FIXED_BINDING_DISTANCE],
|
point(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
@ -861,14 +869,14 @@ export const avoidRectangularCorner = (
|
||||||
) {
|
) {
|
||||||
// Bottom left
|
// Bottom left
|
||||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x, element.y + element.height + FIXED_BINDING_DISTANCE],
|
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x - FIXED_BINDING_DISTANCE, element.y + element.height],
|
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
@ -881,20 +889,20 @@ export const avoidRectangularCorner = (
|
||||||
nonRotatedPoint[0] - element.x <
|
nonRotatedPoint[0] - element.x <
|
||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[
|
point(
|
||||||
element.x + element.width,
|
element.x + element.width,
|
||||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||||
],
|
),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[
|
point(
|
||||||
element.x + element.width + FIXED_BINDING_DISTANCE,
|
element.x + element.width + FIXED_BINDING_DISTANCE,
|
||||||
element.y + element.height,
|
element.y + element.height,
|
||||||
],
|
),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
@ -907,14 +915,14 @@ export const avoidRectangularCorner = (
|
||||||
nonRotatedPoint[0] - element.x <
|
nonRotatedPoint[0] - element.x <
|
||||||
element.width + FIXED_BINDING_DISTANCE
|
element.width + FIXED_BINDING_DISTANCE
|
||||||
) {
|
) {
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x + element.width, element.y - FIXED_BINDING_DISTANCE],
|
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x + element.width + FIXED_BINDING_DISTANCE, element.y],
|
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
@ -925,12 +933,12 @@ export const avoidRectangularCorner = (
|
||||||
|
|
||||||
export const snapToMid = (
|
export const snapToMid = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: Point,
|
p: GlobalPoint,
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = [x + width / 2 - 0.1, y + height / 2 - 0.1] as Point;
|
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
||||||
const nonRotated = rotatePoint(p, center, -angle);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
// above and below certain px distance
|
// above and below certain px distance
|
||||||
|
@ -943,22 +951,30 @@ export const snapToMid = (
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThrehsold
|
||||||
) {
|
) {
|
||||||
// LEFT
|
// LEFT
|
||||||
return rotatePoint([x - FIXED_BINDING_DISTANCE, center[1]], center, angle);
|
return pointRotateRads(
|
||||||
|
point(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[1] <= y + height / 2 &&
|
nonRotated[1] <= y + height / 2 &&
|
||||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
nonRotated[0] > center[0] - horizontalThrehsold &&
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThrehsold
|
||||||
) {
|
) {
|
||||||
// TOP
|
// TOP
|
||||||
return rotatePoint([center[0], y - FIXED_BINDING_DISTANCE], center, angle);
|
return pointRotateRads(
|
||||||
|
point(center[0], y - FIXED_BINDING_DISTANCE),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[0] >= x + width / 2 &&
|
nonRotated[0] >= x + width / 2 &&
|
||||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
nonRotated[1] > center[1] - verticalThrehsold &&
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThrehsold
|
||||||
) {
|
) {
|
||||||
// RIGHT
|
// RIGHT
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[x + width + FIXED_BINDING_DISTANCE, center[1]],
|
point(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
|
@ -968,8 +984,8 @@ export const snapToMid = (
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThrehsold
|
||||||
) {
|
) {
|
||||||
// DOWN
|
// DOWN
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[center[0], y + height + FIXED_BINDING_DISTANCE],
|
point(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
|
@ -984,7 +1000,7 @@ const updateBoundPoint = (
|
||||||
binding: PointBinding | null | undefined,
|
binding: PointBinding | null | undefined,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point | null => {
|
): LocalPoint | null => {
|
||||||
if (
|
if (
|
||||||
binding == null ||
|
binding == null ||
|
||||||
// We only need to update the other end if this is a 2 point line element
|
// We only need to update the other end if this is a 2 point line element
|
||||||
|
@ -1006,15 +1022,15 @@ const updateBoundPoint = (
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = [
|
const globalMidPoint = point<GlobalPoint>(
|
||||||
bindableElement.x + bindableElement.width / 2,
|
bindableElement.x + bindableElement.width / 2,
|
||||||
bindableElement.y + bindableElement.height / 2,
|
bindableElement.y + bindableElement.height / 2,
|
||||||
] as Point;
|
);
|
||||||
const global = [
|
const global = point<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
] as Point;
|
);
|
||||||
const rotatedGlobal = rotatePoint(
|
const rotatedGlobal = pointRotateRads(
|
||||||
global,
|
global,
|
||||||
globalMidPoint,
|
globalMidPoint,
|
||||||
bindableElement.angle,
|
bindableElement.angle,
|
||||||
|
@ -1040,7 +1056,7 @@ const updateBoundPoint = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
let newEdgePoint: Point;
|
let newEdgePoint: GlobalPoint;
|
||||||
|
|
||||||
// The linear element was not originally pointing inside the bound shape,
|
// The linear element was not originally pointing inside the bound shape,
|
||||||
// we can point directly at the focus point
|
// we can point directly at the focus point
|
||||||
|
@ -1054,7 +1070,7 @@ const updateBoundPoint = (
|
||||||
binding.gap,
|
binding.gap,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (intersections.length === 0) {
|
if (!intersections || intersections.length === 0) {
|
||||||
// This should never happen, since focusPoint should always be
|
// This should never happen, since focusPoint should always be
|
||||||
// inside the element, but just in case, bail out
|
// inside the element, but just in case, bail out
|
||||||
newEdgePoint = focusPointAbsolute;
|
newEdgePoint = focusPointAbsolute;
|
||||||
|
@ -1101,15 +1117,15 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const globalMidPoint = [
|
const globalMidPoint = point(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||||
] as Point;
|
);
|
||||||
const nonRotatedSnappedGlobalPoint = rotatePoint(
|
const nonRotatedSnappedGlobalPoint = pointRotateRads(
|
||||||
snappedPoint,
|
snappedPoint,
|
||||||
globalMidPoint,
|
globalMidPoint,
|
||||||
-hoveredElement.angle,
|
-hoveredElement.angle as Radians,
|
||||||
) as Point;
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fixedPoint: normalizeFixedPoint([
|
fixedPoint: normalizeFixedPoint([
|
||||||
|
@ -1320,8 +1336,9 @@ export const bindingBorderTest = (
|
||||||
const threshold = maxBindingGap(element, element.width, element.height);
|
const threshold = maxBindingGap(element, element.width, element.height);
|
||||||
const shape = getElementShape(element, elementsMap);
|
const shape = getElementShape(element, elementsMap);
|
||||||
return (
|
return (
|
||||||
isPointOnShape([x, y], shape, threshold) ||
|
isPointOnShape(point(x, y), shape, threshold) ||
|
||||||
(fullShape === true && pointInsideBounds([x, y], aabbForElement(element)))
|
(fullShape === true &&
|
||||||
|
pointInsideBounds(point(x, y), aabbForElement(element)))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1339,7 +1356,7 @@ export const maxBindingGap = (
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToBindableElement = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): number => {
|
): number => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
@ -1359,19 +1376,13 @@ export const distanceToBindableElement = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const distanceToRectangle = (
|
const distanceToRectangle = (
|
||||||
element:
|
element: ExcalidrawRectanguloidElement,
|
||||||
| ExcalidrawRectangleElement
|
p: GlobalPoint,
|
||||||
| ExcalidrawTextElement
|
|
||||||
| ExcalidrawFreeDrawElement
|
|
||||||
| ExcalidrawImageElement
|
|
||||||
| ExcalidrawIframeLikeElement
|
|
||||||
| ExcalidrawFrameLikeElement,
|
|
||||||
point: Point,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): number => {
|
): number => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||||
element,
|
element,
|
||||||
point,
|
p,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
return Math.max(
|
return Math.max(
|
||||||
|
@ -1382,7 +1393,7 @@ const distanceToRectangle = (
|
||||||
|
|
||||||
const distanceToDiamond = (
|
const distanceToDiamond = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): number => {
|
): number => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||||
|
@ -1396,7 +1407,7 @@ const distanceToDiamond = (
|
||||||
|
|
||||||
const distanceToEllipse = (
|
const distanceToEllipse = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): number => {
|
): number => {
|
||||||
const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
|
const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
|
||||||
|
@ -1405,7 +1416,7 @@ const distanceToEllipse = (
|
||||||
|
|
||||||
const ellipseParamsForTest = (
|
const ellipseParamsForTest = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): [GA.Point, GA.Line] => {
|
): [GA.Point, GA.Line] => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||||
|
@ -1467,7 +1478,7 @@ const ellipseParamsForTest = (
|
||||||
// so we only need to perform hit tests for the positive quadrant.
|
// so we only need to perform hit tests for the positive quadrant.
|
||||||
const pointRelativeToElement = (
|
const pointRelativeToElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
pointTuple: Point,
|
pointTuple: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): [GA.Point, GA.Point, number, number] => {
|
): [GA.Point, GA.Point, number, number] => {
|
||||||
const point = GAPoint.from(pointTuple);
|
const point = GAPoint.from(pointTuple);
|
||||||
|
@ -1516,9 +1527,9 @@ const coordsCenter = (
|
||||||
const determineFocusDistance = (
|
const determineFocusDistance = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
// Point on the line, in absolute coordinates
|
// Point on the line, in absolute coordinates
|
||||||
a: Point,
|
a: GlobalPoint,
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: Point,
|
b: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): number => {
|
): number => {
|
||||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||||
|
@ -1559,13 +1570,13 @@ const determineFocusPoint = (
|
||||||
// The oriented, relative distance from the center of `element` of the
|
// The oriented, relative distance from the center of `element` of the
|
||||||
// returned focusPoint
|
// returned focusPoint
|
||||||
focus: number,
|
focus: number,
|
||||||
adjecentPoint: Point,
|
adjecentPoint: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
const center = coordsCenter(x1, y1, x2, y2);
|
const center = coordsCenter(x1, y1, x2, y2);
|
||||||
return GAPoint.toTuple(center);
|
return pointFromPair(GAPoint.toTuple(center));
|
||||||
}
|
}
|
||||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||||
const adjecentPointRel = GATransform.apply(
|
const adjecentPointRel = GATransform.apply(
|
||||||
|
@ -1589,7 +1600,9 @@ const determineFocusPoint = (
|
||||||
point = findFocusPointForEllipse(element, focus, adjecentPointRel);
|
point = findFocusPointForEllipse(element, focus, adjecentPointRel);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point));
|
return pointFromPair(
|
||||||
|
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns 2 or 0 intersection points between line going through `a` and `b`
|
// Returns 2 or 0 intersection points between line going through `a` and `b`
|
||||||
|
@ -1597,15 +1610,15 @@ const determineFocusPoint = (
|
||||||
const intersectElementWithLine = (
|
const intersectElementWithLine = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
// Point on the line, in absolute coordinates
|
// Point on the line, in absolute coordinates
|
||||||
a: Point,
|
a: GlobalPoint,
|
||||||
// Another point on the line, in absolute coordinates
|
// Another point on the line, in absolute coordinates
|
||||||
b: Point,
|
b: GlobalPoint,
|
||||||
// If given, the element is inflated by this value
|
// If given, the element is inflated by this value
|
||||||
gap: number = 0,
|
gap: number = 0,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point[] => {
|
): GlobalPoint[] | undefined => {
|
||||||
if (isRectangularElement(element)) {
|
if (isRectangularElement(element)) {
|
||||||
return segmentIntersectRectangleElement(element, [a, b], gap);
|
return segmentIntersectRectangleElement(element, lineSegment(a, b), gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||||
|
@ -1619,8 +1632,14 @@ const intersectElementWithLine = (
|
||||||
aRel,
|
aRel,
|
||||||
gap,
|
gap,
|
||||||
);
|
);
|
||||||
return intersections.map((point) =>
|
return intersections.map(
|
||||||
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)),
|
(point) =>
|
||||||
|
pointFromPair(
|
||||||
|
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)),
|
||||||
|
),
|
||||||
|
// pointFromArray(
|
||||||
|
// ,
|
||||||
|
// ),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2173,12 +2192,18 @@ export class BindableElement {
|
||||||
export const getGlobalFixedPointForBindableElement = (
|
export const getGlobalFixedPointForBindableElement = (
|
||||||
fixedPointRatio: [number, number],
|
fixedPointRatio: [number, number],
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
) => {
|
): GlobalPoint => {
|
||||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||||
|
|
||||||
return rotatePoint(
|
return pointRotateRads(
|
||||||
[element.x + element.width * fixedX, element.y + element.height * fixedY],
|
point(
|
||||||
getCenterForElement(element),
|
element.x + element.width * fixedX,
|
||||||
|
element.y + element.height * fixedY,
|
||||||
|
),
|
||||||
|
point<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2186,7 +2211,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||||
const getGlobalFixedPoints = (
|
const getGlobalFixedPoints = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
): [GlobalPoint, GlobalPoint] => {
|
||||||
const startElement =
|
const startElement =
|
||||||
arrow.startBinding &&
|
arrow.startBinding &&
|
||||||
(elementsMap.get(arrow.startBinding.elementId) as
|
(elementsMap.get(arrow.startBinding.elementId) as
|
||||||
|
@ -2197,23 +2222,26 @@ const getGlobalFixedPoints = (
|
||||||
(elementsMap.get(arrow.endBinding.elementId) as
|
(elementsMap.get(arrow.endBinding.elementId) as
|
||||||
| ExcalidrawBindableElement
|
| ExcalidrawBindableElement
|
||||||
| undefined);
|
| undefined);
|
||||||
const startPoint: Point =
|
const startPoint =
|
||||||
startElement && arrow.startBinding
|
startElement && arrow.startBinding
|
||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.startBinding.fixedPoint,
|
arrow.startBinding.fixedPoint,
|
||||||
startElement as ExcalidrawBindableElement,
|
startElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: [arrow.x + arrow.points[0][0], arrow.y + arrow.points[0][1]];
|
: point<GlobalPoint>(
|
||||||
const endPoint: Point =
|
arrow.x + arrow.points[0][0],
|
||||||
|
arrow.y + arrow.points[0][1],
|
||||||
|
);
|
||||||
|
const endPoint =
|
||||||
endElement && arrow.endBinding
|
endElement && arrow.endBinding
|
||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.endBinding.fixedPoint,
|
arrow.endBinding.fixedPoint,
|
||||||
endElement as ExcalidrawBindableElement,
|
endElement as ExcalidrawBindableElement,
|
||||||
)
|
)
|
||||||
: [
|
: point<GlobalPoint>(
|
||||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||||
arrow.y + arrow.points[arrow.points.length - 1][1],
|
arrow.y + arrow.points[arrow.points.length - 1][1],
|
||||||
];
|
);
|
||||||
|
|
||||||
return [startPoint, endPoint];
|
return [startPoint, endPoint];
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
|
import { point } from "../../math";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
|
@ -123,9 +125,9 @@ describe("getElementBounds", () => {
|
||||||
a: 0.6447741904932416,
|
a: 0.6447741904932416,
|
||||||
}),
|
}),
|
||||||
points: [
|
points: [
|
||||||
[0, 0] as [number, number],
|
point<LocalPoint>(0, 0),
|
||||||
[67.33984375, 92.48828125] as [number, number],
|
point<LocalPoint>(67.33984375, 92.48828125),
|
||||||
[-102.7890625, 52.15625] as [number, number],
|
point<LocalPoint>(-102.7890625, 52.15625),
|
||||||
],
|
],
|
||||||
} as ExcalidrawLinearElement;
|
} as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ import type {
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { distance2d, rotate, rotatePoint } from "../math";
|
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { AppState, Point } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { generateRoughOptions } from "../scene/Shape";
|
import { generateRoughOptions } from "../scene/Shape";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
@ -22,9 +22,24 @@ import {
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap, invariant } from "../utils";
|
||||||
|
import type {
|
||||||
|
Degrees,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
|
Radians,
|
||||||
|
} from "../../math";
|
||||||
|
import {
|
||||||
|
degreesToRadians,
|
||||||
|
lineSegment,
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
pointFromArray,
|
||||||
|
pointRotateRads,
|
||||||
|
} from "../../math";
|
||||||
|
import type { Mutable } from "../utility-types";
|
||||||
|
|
||||||
export type RectangleBox = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -97,7 +112,11 @@ export class ElementBounds {
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||||
element.points.map(([x, y]) =>
|
element.points.map(([x, y]) =>
|
||||||
rotate(x, y, cx - element.x, cy - element.y, element.angle),
|
pointRotateRads(
|
||||||
|
point(x, y),
|
||||||
|
point(cx - element.x, cy - element.y),
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -110,10 +129,26 @@ export class ElementBounds {
|
||||||
} else if (isLinearElement(element)) {
|
} else if (isLinearElement(element)) {
|
||||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||||
} else if (element.type === "diamond") {
|
} else if (element.type === "diamond") {
|
||||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
const [x11, y11] = pointRotateRads(
|
||||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
point(cx, y1),
|
||||||
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
|
point(cx, cy),
|
||||||
const [x21, y21] = rotate(x2, cy, cx, cy, element.angle);
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x12, y12] = pointRotateRads(
|
||||||
|
point(cx, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x22, y22] = pointRotateRads(
|
||||||
|
point(x1, cy),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x21, y21] = pointRotateRads(
|
||||||
|
point(x2, cy),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
const minY = Math.min(y11, y12, y22, y21);
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
|
@ -128,10 +163,26 @@ export class ElementBounds {
|
||||||
const hh = Math.hypot(h * cos, w * sin);
|
const hh = Math.hypot(h * cos, w * sin);
|
||||||
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||||
} else {
|
} else {
|
||||||
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
|
const [x11, y11] = pointRotateRads(
|
||||||
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
|
point(x1, y1),
|
||||||
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
|
point(cx, cy),
|
||||||
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x12, y12] = pointRotateRads(
|
||||||
|
point(x1, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x22, y22] = pointRotateRads(
|
||||||
|
point(x2, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [x21, y21] = pointRotateRads(
|
||||||
|
point(x2, y1),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
const minY = Math.min(y11, y12, y22, y21);
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
|
@ -165,18 +216,18 @@ export const getElementAbsoluteCoords = (
|
||||||
? getContainerElement(element, elementsMap)
|
? getContainerElement(element, elementsMap)
|
||||||
: null;
|
: null;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
const { x, y } = LinearElementEditor.getBoundTextElementPosition(
|
||||||
container,
|
container,
|
||||||
element as ExcalidrawTextElementWithContainer,
|
element as ExcalidrawTextElementWithContainer,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
coords.x,
|
x,
|
||||||
coords.y,
|
y,
|
||||||
coords.x + element.width,
|
x + element.width,
|
||||||
coords.y + element.height,
|
y + element.height,
|
||||||
coords.x + element.width / 2,
|
x + element.width / 2,
|
||||||
coords.y + element.height / 2,
|
y + element.height / 2,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,38 +249,40 @@ export const getElementAbsoluteCoords = (
|
||||||
export const getElementLineSegments = (
|
export const getElementLineSegments = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): [Point, Point][] => {
|
): LineSegment<GlobalPoint>[] => {
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center: Point = [cx, cy];
|
const center: GlobalPoint = point(cx, cy);
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
const segments: [Point, Point][] = [];
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < element.points.length - 1) {
|
while (i < element.points.length - 1) {
|
||||||
segments.push([
|
segments.push(
|
||||||
rotatePoint(
|
lineSegment(
|
||||||
[
|
pointRotateRads(
|
||||||
element.points[i][0] + element.x,
|
point(
|
||||||
element.points[i][1] + element.y,
|
element.points[i][0] + element.x,
|
||||||
] as Point,
|
element.points[i][1] + element.y,
|
||||||
center,
|
),
|
||||||
element.angle,
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
point(
|
||||||
|
element.points[i + 1][0] + element.x,
|
||||||
|
element.points[i + 1][1] + element.y,
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
rotatePoint(
|
);
|
||||||
[
|
|
||||||
element.points[i + 1][0] + element.x,
|
|
||||||
element.points[i + 1][1] + element.y,
|
|
||||||
] as Point,
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,40 +299,40 @@ export const getElementLineSegments = (
|
||||||
[cx, y2],
|
[cx, y2],
|
||||||
[x1, cy],
|
[x1, cy],
|
||||||
[x2, cy],
|
[x2, cy],
|
||||||
] as Point[]
|
] as GlobalPoint[]
|
||||||
).map((point) => rotatePoint(point, center, element.angle));
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
return [
|
return [
|
||||||
[n, w],
|
lineSegment(n, w),
|
||||||
[n, e],
|
lineSegment(n, e),
|
||||||
[s, w],
|
lineSegment(s, w),
|
||||||
[s, e],
|
lineSegment(s, e),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
return [
|
return [
|
||||||
[n, w],
|
lineSegment(n, w),
|
||||||
[n, e],
|
lineSegment(n, e),
|
||||||
[s, w],
|
lineSegment(s, w),
|
||||||
[s, e],
|
lineSegment(s, e),
|
||||||
[n, w],
|
lineSegment(n, w),
|
||||||
[n, e],
|
lineSegment(n, e),
|
||||||
[s, w],
|
lineSegment(s, w),
|
||||||
[s, e],
|
lineSegment(s, e),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[nw, ne],
|
lineSegment(nw, ne),
|
||||||
[sw, se],
|
lineSegment(sw, se),
|
||||||
[nw, sw],
|
lineSegment(nw, sw),
|
||||||
[ne, se],
|
lineSegment(ne, se),
|
||||||
[nw, e],
|
lineSegment(nw, e),
|
||||||
[sw, e],
|
lineSegment(sw, e),
|
||||||
[ne, w],
|
lineSegment(ne, w),
|
||||||
[se, w],
|
lineSegment(se, w),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -386,10 +439,10 @@ const solveQuadratic = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCubicBezierCurveBound = (
|
const getCubicBezierCurveBound = (
|
||||||
p0: Point,
|
p0: GlobalPoint,
|
||||||
p1: Point,
|
p1: GlobalPoint,
|
||||||
p2: Point,
|
p2: GlobalPoint,
|
||||||
p3: Point,
|
p3: GlobalPoint,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
|
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
|
||||||
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
|
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
|
||||||
|
@ -415,9 +468,9 @@ const getCubicBezierCurveBound = (
|
||||||
|
|
||||||
export const getMinMaxXYFromCurvePathOps = (
|
export const getMinMaxXYFromCurvePathOps = (
|
||||||
ops: Op[],
|
ops: Op[],
|
||||||
transformXY?: (x: number, y: number) => [number, number],
|
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
let currentP: Point = [0, 0];
|
let currentP: GlobalPoint = point(0, 0);
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
(limits, { op, data }) => {
|
(limits, { op, data }) => {
|
||||||
|
@ -425,19 +478,21 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||||
// move, bcurveTo, lineTo, and curveTo
|
// move, bcurveTo, lineTo, and curveTo
|
||||||
if (op === "move") {
|
if (op === "move") {
|
||||||
// change starting point
|
// change starting point
|
||||||
currentP = data as unknown as Point;
|
const p: GlobalPoint | undefined = pointFromArray(data);
|
||||||
|
invariant(p != null, "Op data is not a point");
|
||||||
|
currentP = p;
|
||||||
// move operation does not draw anything; so, it always
|
// move operation does not draw anything; so, it always
|
||||||
// returns false
|
// returns false
|
||||||
} else if (op === "bcurveTo") {
|
} else if (op === "bcurveTo") {
|
||||||
const _p1 = [data[0], data[1]] as Point;
|
const _p1 = point<GlobalPoint>(data[0], data[1]);
|
||||||
const _p2 = [data[2], data[3]] as Point;
|
const _p2 = point<GlobalPoint>(data[2], data[3]);
|
||||||
const _p3 = [data[4], data[5]] as Point;
|
const _p3 = point<GlobalPoint>(data[4], data[5]);
|
||||||
|
|
||||||
const p1 = transformXY ? transformXY(..._p1) : _p1;
|
const p1 = transformXY ? transformXY(_p1) : _p1;
|
||||||
const p2 = transformXY ? transformXY(..._p2) : _p2;
|
const p2 = transformXY ? transformXY(_p2) : _p2;
|
||||||
const p3 = transformXY ? transformXY(..._p3) : _p3;
|
const p3 = transformXY ? transformXY(_p3) : _p3;
|
||||||
|
|
||||||
const p0 = transformXY ? transformXY(...currentP) : currentP;
|
const p0 = transformXY ? transformXY(currentP) : currentP;
|
||||||
currentP = _p3;
|
currentP = _p3;
|
||||||
|
|
||||||
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
|
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
|
||||||
|
@ -507,14 +562,14 @@ export const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @returns number in degrees */
|
/** @returns number in degrees */
|
||||||
export const getArrowheadAngle = (arrowhead: Arrowhead): number => {
|
export const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
|
||||||
switch (arrowhead) {
|
switch (arrowhead) {
|
||||||
case "bar":
|
case "bar":
|
||||||
return 90;
|
return 90 as Degrees;
|
||||||
case "arrow":
|
case "arrow":
|
||||||
return 20;
|
return 20 as Degrees;
|
||||||
default:
|
default:
|
||||||
return 25;
|
return 25 as Degrees;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -533,19 +588,24 @@ export const getArrowheadPoints = (
|
||||||
const index = position === "start" ? 1 : ops.length - 1;
|
const index = position === "start" ? 1 : ops.length - 1;
|
||||||
|
|
||||||
const data = ops[index].data;
|
const data = ops[index].data;
|
||||||
const p3 = [data[4], data[5]] as Point;
|
|
||||||
const p2 = [data[2], data[3]] as Point;
|
invariant(data.length === 6, "Op data length is not 6");
|
||||||
const p1 = [data[0], data[1]] as Point;
|
|
||||||
|
const p3 = point(data[4], data[5]);
|
||||||
|
const p2 = point(data[2], data[3]);
|
||||||
|
const p1 = point(data[0], data[1]);
|
||||||
|
|
||||||
// We need to find p0 of the bezier curve.
|
// We need to find p0 of the bezier curve.
|
||||||
// It is typically the last point of the previous
|
// It is typically the last point of the previous
|
||||||
// curve; it can also be the position of moveTo operation.
|
// curve; it can also be the position of moveTo operation.
|
||||||
const prevOp = ops[index - 1];
|
const prevOp = ops[index - 1];
|
||||||
let p0: Point = [0, 0];
|
let p0 = point(0, 0);
|
||||||
if (prevOp.op === "move") {
|
if (prevOp.op === "move") {
|
||||||
p0 = prevOp.data as unknown as Point;
|
const p = pointFromArray(prevOp.data);
|
||||||
|
invariant(p != null, "Op data is not a point");
|
||||||
|
p0 = p;
|
||||||
} else if (prevOp.op === "bcurveTo") {
|
} else if (prevOp.op === "bcurveTo") {
|
||||||
p0 = [prevOp.data[4], prevOp.data[5]];
|
p0 = point(prevOp.data[4], prevOp.data[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||||
|
@ -610,8 +670,16 @@ export const getArrowheadPoints = (
|
||||||
const angle = getArrowheadAngle(arrowhead);
|
const angle = getArrowheadAngle(arrowhead);
|
||||||
|
|
||||||
// Return points
|
// Return points
|
||||||
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
const [x3, y3] = pointRotateRads(
|
||||||
const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
|
point(xs, ys),
|
||||||
|
point(x2, y2),
|
||||||
|
((-angle * Math.PI) / 180) as Radians,
|
||||||
|
);
|
||||||
|
const [x4, y4] = pointRotateRads(
|
||||||
|
point(xs, ys),
|
||||||
|
point(x2, y2),
|
||||||
|
degreesToRadians(angle),
|
||||||
|
);
|
||||||
|
|
||||||
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
||||||
// point opposite to the arrowhead point
|
// point opposite to the arrowhead point
|
||||||
|
@ -621,12 +689,10 @@ export const getArrowheadPoints = (
|
||||||
if (position === "start") {
|
if (position === "start") {
|
||||||
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||||
|
|
||||||
[ox, oy] = rotate(
|
[ox, oy] = pointRotateRads(
|
||||||
x2 + minSize * 2,
|
point(x2 + minSize * 2, y2),
|
||||||
y2,
|
point(x2, y2),
|
||||||
x2,
|
Math.atan2(py - y2, px - x2) as Radians,
|
||||||
y2,
|
|
||||||
Math.atan2(py - y2, px - x2),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [px, py] =
|
const [px, py] =
|
||||||
|
@ -634,12 +700,10 @@ export const getArrowheadPoints = (
|
||||||
? element.points[element.points.length - 2]
|
? element.points[element.points.length - 2]
|
||||||
: [0, 0];
|
: [0, 0];
|
||||||
|
|
||||||
[ox, oy] = rotate(
|
[ox, oy] = pointRotateRads(
|
||||||
x2 - minSize * 2,
|
point(x2 - minSize * 2, y2),
|
||||||
y2,
|
point(x2, y2),
|
||||||
x2,
|
Math.atan2(y2 - py, x2 - px) as Radians,
|
||||||
y2,
|
|
||||||
Math.atan2(y2 - py, x2 - px),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,7 +729,10 @@ const generateLinearElementShape = (
|
||||||
return "linearPath";
|
return "linearPath";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return generator[method](element.points as Mutable<Point>[], options);
|
return generator[method](
|
||||||
|
element.points as Mutable<LocalPoint>[] as RoughPoint[],
|
||||||
|
options,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLinearElementRotatedBounds = (
|
const getLinearElementRotatedBounds = (
|
||||||
|
@ -678,11 +745,9 @@ const getLinearElementRotatedBounds = (
|
||||||
|
|
||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = rotate(
|
const [x, y] = pointRotateRads(
|
||||||
element.x + pointX,
|
point(element.x + pointX, element.y + pointY),
|
||||||
element.y + pointY,
|
point(cx, cy),
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -708,8 +773,12 @@ const getLinearElementRotatedBounds = (
|
||||||
const cachedShape = ShapeCache.get(element)?.[0];
|
const cachedShape = ShapeCache.get(element)?.[0];
|
||||||
const shape = cachedShape ?? generateLinearElementShape(element);
|
const shape = cachedShape ?? generateLinearElementShape(element);
|
||||||
const ops = getCurvePathOps(shape);
|
const ops = getCurvePathOps(shape);
|
||||||
const transformXY = (x: number, y: number) =>
|
const transformXY = ([x, y]: GlobalPoint) =>
|
||||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
pointRotateRads<GlobalPoint>(
|
||||||
|
point(element.x + x, element.y + y),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
|
@ -861,7 +930,10 @@ export const getClosestElementBounds = (
|
||||||
const elementsMap = arrayToMap(elements);
|
const elementsMap = arrayToMap(elements);
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||||
const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
|
const distance = pointDistance(
|
||||||
|
point((x1 + x2) / 2, (y1 + y2) / 2),
|
||||||
|
point(from.x, from.y),
|
||||||
|
);
|
||||||
|
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
|
@ -916,3 +988,9 @@ export const getVisibleSceneBounds = ({
|
||||||
-scrollY + height / zoom.value,
|
-scrollY + height / zoom.value,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
||||||
|
point(
|
||||||
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||||
|
);
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { isPathALoop, isPointWithinBounds } from "../math";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementBounds } from "./bounds";
|
import { getElementBounds } from "./bounds";
|
||||||
import type { FrameNameBounds } from "../types";
|
import type { FrameNameBounds } from "../types";
|
||||||
import type { Polygon, GeometricShape } from "../../utils/geometry/shape";
|
import type { GeometricShape } from "../../utils/geometry/shape";
|
||||||
import { getPolygonShape } from "../../utils/geometry/shape";
|
import { getPolygonShape } from "../../utils/geometry/shape";
|
||||||
import { isPointInShape, isPointOnShape } from "../../utils/collision";
|
import { isPointInShape, isPointOnShape } from "../../utils/collision";
|
||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
|
@ -18,7 +15,9 @@ import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getBoundTextShape } from "../shapes";
|
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||||
|
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
||||||
|
import { isPointWithinBounds, point } from "../../math";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
|
@ -42,35 +41,36 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
return isDraggableFromInside || isImageElement(element);
|
return isDraggableFromInside || isImageElement(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HitTestArgs = {
|
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
shape: GeometricShape;
|
shape: GeometricShape<Point>;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementItself = ({
|
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element,
|
element,
|
||||||
shape,
|
shape,
|
||||||
threshold = 10,
|
threshold = 10,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
}: HitTestArgs) => {
|
}: HitTestArgs<Point>) => {
|
||||||
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([x, y], shape) || isPointOnShape([x, y], shape, threshold)
|
isPointInShape(point(x, y), shape) ||
|
||||||
: isPointOnShape([x, y], shape, threshold);
|
isPointOnShape(point(x, y), shape, threshold)
|
||||||
|
: isPointOnShape(point(x, y), shape, threshold);
|
||||||
|
|
||||||
// hit test against a frame's name
|
// hit test against a frame's name
|
||||||
if (!hit && frameNameBound) {
|
if (!hit && frameNameBound) {
|
||||||
hit = isPointInShape([x, y], {
|
hit = isPointInShape(point(x, y), {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||||
.data as Polygon,
|
.data as Polygon<Point>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +89,13 @@ export const hitElementBoundingBox = (
|
||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds([x1, y1], [x, y], [x2, y2]);
|
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = (
|
export const hitElementBoundingBoxOnly = <
|
||||||
hitArgs: HitTestArgs,
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(
|
||||||
|
hitArgs: HitTestArgs<Point>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
@ -108,10 +110,10 @@ export const hitElementBoundingBoxOnly = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundText = (
|
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
textShape: GeometricShape | null,
|
textShape: GeometricShape<Point> | null,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape([x, y], textShape);
|
return !!textShape && isPointInShape(point(x, y), textShape);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
PointerDownState,
|
PointerDownState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getBoundTextElement, getMinTextElementWidth } from "./textElement";
|
import { getBoundTextElement, getMinTextElementWidth } from "./textElement";
|
||||||
import { getGridPoint } from "../math";
|
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
@ -21,6 +20,7 @@ import {
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
||||||
|
import { getGridPoint } from "../snapping";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
import { bindLinearElement } from "./binding";
|
import { bindLinearElement } from "./binding";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { newArrowElement, newElement } from "./newElement";
|
import { newArrowElement, newElement } from "./newElement";
|
||||||
import { aabbForElement } from "../math";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
@ -20,7 +19,7 @@ import type {
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import type { AppState, PendingExcalidrawElements, Point } from "../types";
|
import type { AppState, PendingExcalidrawElements } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
|
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
|
||||||
import {
|
import {
|
||||||
|
@ -30,6 +29,8 @@ import {
|
||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { invariant } from "../utils";
|
import { invariant } from "../utils";
|
||||||
|
import { point, type LocalPoint } from "../../math";
|
||||||
|
import { aabbForElement } from "../shapes";
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
|
@ -81,13 +82,14 @@ const getNodeRelatives = (
|
||||||
"not an ExcalidrawBindableElement",
|
"not an ExcalidrawBindableElement",
|
||||||
);
|
);
|
||||||
|
|
||||||
const edgePoint: Point =
|
const edgePoint = (
|
||||||
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0];
|
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0]
|
||||||
|
) as Readonly<LocalPoint>;
|
||||||
|
|
||||||
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
||||||
edgePoint[0] + el.x,
|
edgePoint[0] + el.x,
|
||||||
edgePoint[1] + el.y,
|
edgePoint[1] + el.y,
|
||||||
]);
|
] as Readonly<LocalPoint>);
|
||||||
|
|
||||||
acc.push({
|
acc.push({
|
||||||
relative,
|
relative,
|
||||||
|
@ -419,10 +421,7 @@ const createBindingArrow = (
|
||||||
strokeColor: appState.currentItemStrokeColor,
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
strokeWidth: appState.currentItemStrokeWidth,
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
points: [
|
points: [point(0, 0), point(endX, endY)],
|
||||||
[0, 0],
|
|
||||||
[endX, endY],
|
|
||||||
],
|
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { lineAngle } from "../../utils/geometry/geometry";
|
import type {
|
||||||
import type { Point, Vector } from "../../utils/geometry/shape";
|
LocalPoint,
|
||||||
|
GlobalPoint,
|
||||||
|
Triangle,
|
||||||
|
Vector,
|
||||||
|
Radians,
|
||||||
|
} from "../../math";
|
||||||
import {
|
import {
|
||||||
getCenterForBounds,
|
point,
|
||||||
PointInTriangle,
|
pointRotateRads,
|
||||||
rotatePoint,
|
pointScaleFromOrigin,
|
||||||
scalePointFromOrigin,
|
radiansToDegrees,
|
||||||
} from "../math";
|
triangleIncludesPoint,
|
||||||
import type { Bounds } from "./bounds";
|
} from "../../math";
|
||||||
|
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||||
import type { ExcalidrawBindableElement } from "./types";
|
import type { ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
export const HEADING_RIGHT = [1, 0] as Heading;
|
export const HEADING_RIGHT = [1, 0] as Heading;
|
||||||
|
@ -15,8 +21,13 @@ 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 = (a: Point, b: Point) => {
|
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
|
||||||
const angle = lineAngle([a, b]);
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
) => {
|
||||||
|
const angle = radiansToDegrees(
|
||||||
|
Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians,
|
||||||
|
);
|
||||||
if (angle >= 315 || angle < 45) {
|
if (angle >= 315 || angle < 45) {
|
||||||
return HEADING_UP;
|
return HEADING_UP;
|
||||||
} else if (angle >= 45 && angle < 135) {
|
} else if (angle >= 45 && angle < 135) {
|
||||||
|
@ -47,56 +58,58 @@ export const compareHeading = (a: Heading, b: Heading) =>
|
||||||
// Gets the heading for the point by creating a bounding box around the rotated
|
// Gets the heading for the point by creating a bounding box around the rotated
|
||||||
// close fitting bounding box, then creating 4 search cones around the center of
|
// close fitting bounding box, then creating 4 search cones around the center of
|
||||||
// the external bbox.
|
// the external bbox.
|
||||||
export const headingForPointFromElement = (
|
export const headingForPointFromElement = <
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(
|
||||||
element: Readonly<ExcalidrawBindableElement>,
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
aabb: Readonly<Bounds>,
|
aabb: Readonly<Bounds>,
|
||||||
point: Readonly<Point>,
|
p: Readonly<LocalPoint | GlobalPoint>,
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const SEARCH_CONE_MULTIPLIER = 2;
|
const SEARCH_CONE_MULTIPLIER = 2;
|
||||||
|
|
||||||
const midPoint = getCenterForBounds(aabb);
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
if (point[0] < element.x) {
|
if (p[0] < element.x) {
|
||||||
return HEADING_LEFT;
|
return HEADING_LEFT;
|
||||||
} else if (point[1] < element.y) {
|
} else if (p[1] < element.y) {
|
||||||
return HEADING_UP;
|
return HEADING_UP;
|
||||||
} else if (point[0] > element.x + element.width) {
|
} else if (p[0] > element.x + element.width) {
|
||||||
return HEADING_RIGHT;
|
return HEADING_RIGHT;
|
||||||
} else if (point[1] > element.y + element.height) {
|
} else if (p[1] > element.y + element.height) {
|
||||||
return HEADING_DOWN;
|
return HEADING_DOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = rotatePoint(
|
const top = pointRotateRads(
|
||||||
scalePointFromOrigin(
|
pointScaleFromOrigin(
|
||||||
[element.x + element.width / 2, element.y],
|
point(element.x + element.width / 2, element.y),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
midPoint,
|
midPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const right = rotatePoint(
|
const right = pointRotateRads(
|
||||||
scalePointFromOrigin(
|
pointScaleFromOrigin(
|
||||||
[element.x + element.width, element.y + element.height / 2],
|
point(element.x + element.width, element.y + element.height / 2),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
midPoint,
|
midPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottom = rotatePoint(
|
const bottom = pointRotateRads(
|
||||||
scalePointFromOrigin(
|
pointScaleFromOrigin(
|
||||||
[element.x + element.width / 2, element.y + element.height],
|
point(element.x + element.width / 2, element.y + element.height),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
midPoint,
|
midPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const left = rotatePoint(
|
const left = pointRotateRads(
|
||||||
scalePointFromOrigin(
|
pointScaleFromOrigin(
|
||||||
[element.x, element.y + element.height / 2],
|
point(element.x, element.y + element.height / 2),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
),
|
),
|
||||||
|
@ -104,43 +117,62 @@ export const headingForPointFromElement = (
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (PointInTriangle(point, top, right, midPoint)) {
|
if (triangleIncludesPoint([top, right, midPoint] as Triangle<Point>, p)) {
|
||||||
return headingForDiamond(top, right);
|
return headingForDiamond(top, right);
|
||||||
} else if (PointInTriangle(point, right, bottom, midPoint)) {
|
} else if (
|
||||||
|
triangleIncludesPoint([right, bottom, midPoint] as Triangle<Point>, p)
|
||||||
|
) {
|
||||||
return headingForDiamond(right, bottom);
|
return headingForDiamond(right, bottom);
|
||||||
} else if (PointInTriangle(point, bottom, left, midPoint)) {
|
} else if (
|
||||||
|
triangleIncludesPoint([bottom, left, midPoint] as Triangle<Point>, p)
|
||||||
|
) {
|
||||||
return headingForDiamond(bottom, left);
|
return headingForDiamond(bottom, left);
|
||||||
}
|
}
|
||||||
|
|
||||||
return headingForDiamond(left, top);
|
return headingForDiamond(left, top);
|
||||||
}
|
}
|
||||||
|
|
||||||
const topLeft = scalePointFromOrigin(
|
const topLeft = pointScaleFromOrigin(
|
||||||
[aabb[0], aabb[1]],
|
point(aabb[0], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
);
|
) as Point;
|
||||||
const topRight = scalePointFromOrigin(
|
const topRight = pointScaleFromOrigin(
|
||||||
[aabb[2], aabb[1]],
|
point(aabb[2], aabb[1]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
);
|
) as Point;
|
||||||
const bottomLeft = scalePointFromOrigin(
|
const bottomLeft = pointScaleFromOrigin(
|
||||||
[aabb[0], aabb[3]],
|
point(aabb[0], aabb[3]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
);
|
) as Point;
|
||||||
const bottomRight = scalePointFromOrigin(
|
const bottomRight = pointScaleFromOrigin(
|
||||||
[aabb[2], aabb[3]],
|
point(aabb[2], aabb[3]),
|
||||||
midPoint,
|
midPoint,
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
);
|
) as Point;
|
||||||
|
|
||||||
return PointInTriangle(point, topLeft, topRight, midPoint)
|
return triangleIncludesPoint(
|
||||||
|
[topLeft, topRight, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
? HEADING_UP
|
? HEADING_UP
|
||||||
: PointInTriangle(point, topRight, bottomRight, midPoint)
|
: triangleIncludesPoint(
|
||||||
|
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
? HEADING_RIGHT
|
? HEADING_RIGHT
|
||||||
: PointInTriangle(point, bottomRight, bottomLeft, midPoint)
|
: triangleIncludesPoint(
|
||||||
|
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
? HEADING_DOWN
|
? HEADING_DOWN
|
||||||
: HEADING_LEFT;
|
: HEADING_LEFT;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const flipHeading = (h: Heading): Heading =>
|
||||||
|
[
|
||||||
|
h[0] === 0 ? 0 : h[0] > 0 ? -1 : 1,
|
||||||
|
h[1] === 0 ? 0 : h[1] > 0 ? -1 : 1,
|
||||||
|
] as Heading;
|
||||||
|
|
|
@ -11,19 +11,6 @@ import type {
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
|
||||||
distance2d,
|
|
||||||
rotate,
|
|
||||||
isPathALoop,
|
|
||||||
getGridPoint,
|
|
||||||
rotatePoint,
|
|
||||||
centerPoint,
|
|
||||||
getControlPointsForBezierCurve,
|
|
||||||
getBezierXY,
|
|
||||||
getBezierCurveLength,
|
|
||||||
mapIntervalToBezierT,
|
|
||||||
arePointsEqual,
|
|
||||||
} from "../math";
|
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import {
|
import {
|
||||||
|
@ -32,7 +19,6 @@ import {
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
Point,
|
|
||||||
AppState,
|
AppState,
|
||||||
PointerCoords,
|
PointerCoords,
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
|
@ -46,7 +32,7 @@ import {
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { toBrandedType, tupleToCoors } from "../utils";
|
import { invariant, toBrandedType, tupleToCoors } from "../utils";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
@ -60,10 +46,29 @@ import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import type { Store } from "../store";
|
import type { Store } from "../store";
|
||||||
import { mutateElbowArrow } from "./routing";
|
import { mutateElbowArrow } from "./routing";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
|
import type { Radians } from "../../math";
|
||||||
|
import {
|
||||||
|
pointCenter,
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
pointsEqual,
|
||||||
|
vector,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
pointDistance,
|
||||||
|
} from "../../math";
|
||||||
|
import {
|
||||||
|
getBezierCurveLength,
|
||||||
|
getBezierXY,
|
||||||
|
getControlPointsForBezierCurve,
|
||||||
|
isPathALoop,
|
||||||
|
mapIntervalToBezierT,
|
||||||
|
} from "../shapes";
|
||||||
|
import { getGridPoint } from "../snapping";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
points: (Point | null)[];
|
points: (GlobalPoint | null)[];
|
||||||
zoom: number | null;
|
zoom: number | null;
|
||||||
} = { version: null, points: [], zoom: null };
|
} = { version: null, points: [], zoom: null };
|
||||||
export class LinearElementEditor {
|
export class LinearElementEditor {
|
||||||
|
@ -80,7 +85,7 @@ export class LinearElementEditor {
|
||||||
lastClickedIsEndPoint: boolean;
|
lastClickedIsEndPoint: boolean;
|
||||||
origin: Readonly<{ x: number; y: number }> | null;
|
origin: Readonly<{ x: number; y: number }> | null;
|
||||||
segmentMidpoint: {
|
segmentMidpoint: {
|
||||||
value: Point | null;
|
value: GlobalPoint | null;
|
||||||
index: number | null;
|
index: number | null;
|
||||||
added: boolean;
|
added: boolean;
|
||||||
};
|
};
|
||||||
|
@ -88,7 +93,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: Point | null;
|
public readonly lastUncommittedPoint: LocalPoint | null;
|
||||||
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||||
public readonly startBindingElement:
|
public readonly startBindingElement:
|
||||||
| ExcalidrawBindableElement
|
| ExcalidrawBindableElement
|
||||||
|
@ -96,13 +101,13 @@ export class LinearElementEditor {
|
||||||
| "keep";
|
| "keep";
|
||||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||||
public readonly hoverPointIndex: number;
|
public readonly hoverPointIndex: number;
|
||||||
public readonly segmentMidPointHoveredCoords: Point | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
if (!arePointsEqual(element.points[0], [0, 0])) {
|
if (!pointsEqual(element.points[0], point(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +285,7 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
referencePoint,
|
referencePoint,
|
||||||
[scenePointerX, scenePointerY],
|
point(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -289,7 +294,10 @@ export class LinearElementEditor {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
point: [width + referencePoint[0], height + referencePoint[1]],
|
point: point(
|
||||||
|
width + referencePoint[0],
|
||||||
|
height + referencePoint[1],
|
||||||
|
),
|
||||||
isDragging: selectedIndex === lastClickedPoint,
|
isDragging: selectedIndex === lastClickedPoint,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -310,7 +318,7 @@ export class LinearElementEditor {
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition =
|
const newPointPosition: LocalPoint =
|
||||||
pointIndex === lastClickedPoint
|
pointIndex === lastClickedPoint
|
||||||
? LinearElementEditor.createPointAt(
|
? LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
|
@ -319,10 +327,10 @@ export class LinearElementEditor {
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
)
|
)
|
||||||
: ([
|
: point(
|
||||||
element.points[pointIndex][0] + deltaX,
|
element.points[pointIndex][0] + deltaX,
|
||||||
element.points[pointIndex][1] + deltaY,
|
element.points[pointIndex][1] + deltaY,
|
||||||
] as const);
|
);
|
||||||
return {
|
return {
|
||||||
index: pointIndex,
|
index: pointIndex,
|
||||||
point: newPointPosition,
|
point: newPointPosition,
|
||||||
|
@ -515,7 +523,7 @@ export class LinearElementEditor {
|
||||||
);
|
);
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const midpoints: (Point | null)[] = [];
|
const midpoints: (GlobalPoint | null)[] = [];
|
||||||
while (index < points.length - 1) {
|
while (index < points.length - 1) {
|
||||||
if (
|
if (
|
||||||
LinearElementEditor.isSegmentTooShort(
|
LinearElementEditor.isSegmentTooShort(
|
||||||
|
@ -549,7 +557,7 @@ export class LinearElementEditor {
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
): GlobalPoint | null => {
|
||||||
const { elementId } = linearElementEditor;
|
const { elementId } = linearElementEditor;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
@ -579,11 +587,12 @@ export class LinearElementEditor {
|
||||||
const existingSegmentMidpointHitCoords =
|
const existingSegmentMidpointHitCoords =
|
||||||
linearElementEditor.segmentMidPointHoveredCoords;
|
linearElementEditor.segmentMidPointHoveredCoords;
|
||||||
if (existingSegmentMidpointHitCoords) {
|
if (existingSegmentMidpointHitCoords) {
|
||||||
const distance = distance2d(
|
const distance = pointDistance(
|
||||||
existingSegmentMidpointHitCoords[0],
|
point(
|
||||||
existingSegmentMidpointHitCoords[1],
|
existingSegmentMidpointHitCoords[0],
|
||||||
scenePointer.x,
|
existingSegmentMidpointHitCoords[1],
|
||||||
scenePointer.y,
|
),
|
||||||
|
point(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
if (distance <= threshold) {
|
if (distance <= threshold) {
|
||||||
return existingSegmentMidpointHitCoords;
|
return existingSegmentMidpointHitCoords;
|
||||||
|
@ -594,11 +603,9 @@ export class LinearElementEditor {
|
||||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (midPoints[index] !== null) {
|
if (midPoints[index] !== null) {
|
||||||
const distance = distance2d(
|
const distance = pointDistance(
|
||||||
midPoints[index]![0],
|
point(midPoints[index]![0], midPoints[index]![1]),
|
||||||
midPoints[index]![1],
|
point(scenePointer.x, scenePointer.y),
|
||||||
scenePointer.x,
|
|
||||||
scenePointer.y,
|
|
||||||
);
|
);
|
||||||
if (distance <= threshold) {
|
if (distance <= threshold) {
|
||||||
return midPoints[index];
|
return midPoints[index];
|
||||||
|
@ -612,15 +619,13 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static isSegmentTooShort(
|
static isSegmentTooShort(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startPoint: Point,
|
startPoint: GlobalPoint | LocalPoint,
|
||||||
endPoint: Point,
|
endPoint: GlobalPoint | LocalPoint,
|
||||||
zoom: AppState["zoom"],
|
zoom: AppState["zoom"],
|
||||||
) {
|
) {
|
||||||
let distance = distance2d(
|
let distance = pointDistance(
|
||||||
startPoint[0],
|
point(startPoint[0], startPoint[1]),
|
||||||
startPoint[1],
|
point(endPoint[0], endPoint[1]),
|
||||||
endPoint[0],
|
|
||||||
endPoint[1],
|
|
||||||
);
|
);
|
||||||
if (element.points.length > 2 && element.roundness) {
|
if (element.points.length > 2 && element.roundness) {
|
||||||
distance = getBezierCurveLength(element, endPoint);
|
distance = getBezierCurveLength(element, endPoint);
|
||||||
|
@ -631,12 +636,12 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static getSegmentMidPoint(
|
static getSegmentMidPoint(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startPoint: Point,
|
startPoint: GlobalPoint,
|
||||||
endPoint: Point,
|
endPoint: GlobalPoint,
|
||||||
endPointIndex: number,
|
endPointIndex: number,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) {
|
): GlobalPoint {
|
||||||
let segmentMidPoint = centerPoint(startPoint, endPoint);
|
let segmentMidPoint = pointCenter(startPoint, endPoint);
|
||||||
if (element.points.length > 2 && element.roundness) {
|
if (element.points.length > 2 && element.roundness) {
|
||||||
const controlPoints = getControlPointsForBezierCurve(
|
const controlPoints = getControlPointsForBezierCurve(
|
||||||
element,
|
element,
|
||||||
|
@ -649,16 +654,15 @@ export class LinearElementEditor {
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [tx, ty] = getBezierXY(
|
|
||||||
controlPoints[0],
|
|
||||||
controlPoints[1],
|
|
||||||
controlPoints[2],
|
|
||||||
controlPoints[3],
|
|
||||||
t,
|
|
||||||
);
|
|
||||||
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
|
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
[tx, ty],
|
getBezierXY(
|
||||||
|
controlPoints[0],
|
||||||
|
controlPoints[1],
|
||||||
|
controlPoints[2],
|
||||||
|
controlPoints[3],
|
||||||
|
t,
|
||||||
|
),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -670,7 +674,7 @@ export class LinearElementEditor {
|
||||||
static getSegmentMidPointIndex(
|
static getSegmentMidPointIndex(
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
midPoint: Point,
|
midPoint: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) {
|
) {
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
|
@ -822,11 +826,12 @@ export class LinearElementEditor {
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const targetPoint =
|
const targetPoint =
|
||||||
clickedPointIndex > -1 &&
|
clickedPointIndex > -1 &&
|
||||||
rotate(
|
pointRotateRads(
|
||||||
element.x + element.points[clickedPointIndex][0],
|
point(
|
||||||
element.y + element.points[clickedPointIndex][1],
|
element.x + element.points[clickedPointIndex][0],
|
||||||
cx,
|
element.y + element.points[clickedPointIndex][1],
|
||||||
cy,
|
),
|
||||||
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -865,14 +870,17 @@ export class LinearElementEditor {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static arePointsEqual(point1: Point | null, point2: Point | null) {
|
static arePointsEqual<Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point1: Point | null,
|
||||||
|
point2: Point | null,
|
||||||
|
) {
|
||||||
if (!point1 && !point2) {
|
if (!point1 && !point2) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!point1 || !point2) {
|
if (!point1 || !point2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return arePointsEqual(point1, point2);
|
return pointsEqual(point1, point2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static handlePointerMove(
|
static handlePointerMove(
|
||||||
|
@ -909,7 +917,7 @@ export class LinearElementEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let newPoint: Point;
|
let newPoint: LocalPoint;
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
||||||
const lastCommittedPoint = points[points.length - 2];
|
const lastCommittedPoint = points[points.length - 2];
|
||||||
|
@ -918,14 +926,14 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
lastCommittedPoint,
|
lastCommittedPoint,
|
||||||
[scenePointerX, scenePointerY],
|
point(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
newPoint = [
|
newPoint = point(
|
||||||
width + lastCommittedPoint[0],
|
width + lastCommittedPoint[0],
|
||||||
height + lastCommittedPoint[1],
|
height + lastCommittedPoint[1],
|
||||||
];
|
);
|
||||||
} else {
|
} else {
|
||||||
newPoint = LinearElementEditor.createPointAt(
|
newPoint = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
|
@ -965,30 +973,36 @@ export class LinearElementEditor {
|
||||||
/** scene coords */
|
/** scene coords */
|
||||||
static getPointGlobalCoordinates(
|
static getPointGlobalCoordinates(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
point: Point,
|
p: LocalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) {
|
): GlobalPoint {
|
||||||
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;
|
||||||
|
|
||||||
let { x, y } = element;
|
const { x, y } = element;
|
||||||
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
return pointRotateRads(
|
||||||
return [x, y] as const;
|
point(x + p[0], y + p[1]),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** scene coords */
|
/** scene coords */
|
||||||
static getPointsGlobalCoordinates(
|
static getPointsGlobalCoordinates(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point[] {
|
): GlobalPoint[] {
|
||||||
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;
|
||||||
return element.points.map((point) => {
|
return element.points.map((p) => {
|
||||||
let { x, y } = element;
|
const { x, y } = element;
|
||||||
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
return pointRotateRads(
|
||||||
return [x, y] as const;
|
point(x + p[0], y + p[1]),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -997,7 +1011,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
indexMaybeFromEnd: number, // -1 for last element
|
indexMaybeFromEnd: number, // -1 for last element
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point {
|
): GlobalPoint {
|
||||||
const index =
|
const index =
|
||||||
indexMaybeFromEnd < 0
|
indexMaybeFromEnd < 0
|
||||||
? element.points.length + indexMaybeFromEnd
|
? element.points.length + indexMaybeFromEnd
|
||||||
|
@ -1005,35 +1019,36 @@ export class LinearElementEditor {
|
||||||
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;
|
||||||
|
const p = element.points[index];
|
||||||
const point = element.points[index];
|
|
||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
return point
|
|
||||||
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
|
return p
|
||||||
: rotate(x, y, cx, cy, element.angle);
|
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
|
||||||
|
: pointRotateRads(point(x, y), point(cx, cy), element.angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pointFromAbsoluteCoords(
|
static pointFromAbsoluteCoords(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
absoluteCoords: Point,
|
absoluteCoords: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point {
|
): LocalPoint {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
// No rotation for elbow arrows
|
// No rotation for elbow arrows
|
||||||
return [absoluteCoords[0] - element.x, absoluteCoords[1] - element.y];
|
return point(
|
||||||
|
absoluteCoords[0] - element.x,
|
||||||
|
absoluteCoords[1] - element.y,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const [x, y] = rotate(
|
const [x, y] = pointRotateRads(
|
||||||
absoluteCoords[0],
|
point(absoluteCoords[0], absoluteCoords[1]),
|
||||||
absoluteCoords[1],
|
point(cx, cy),
|
||||||
cx,
|
-element.angle as Radians,
|
||||||
cy,
|
|
||||||
-element.angle,
|
|
||||||
);
|
);
|
||||||
return [x - element.x, y - element.y];
|
return point(x - element.x, y - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPointIndexUnderCursor(
|
static getPointIndexUnderCursor(
|
||||||
|
@ -1052,9 +1067,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 point = pointHandles[idx];
|
const p = pointHandles[idx];
|
||||||
if (
|
if (
|
||||||
distance2d(x, y, point[0], point[1]) * zoom.value <
|
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
|
||||||
// +1px to account for outline stroke
|
// +1px to account for outline stroke
|
||||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||||
) {
|
) {
|
||||||
|
@ -1070,20 +1085,18 @@ export class LinearElementEditor {
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
gridSize: NullableGridSize,
|
gridSize: NullableGridSize,
|
||||||
): Point {
|
): LocalPoint {
|
||||||
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
|
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, 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;
|
||||||
const [rotatedX, rotatedY] = rotate(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
pointerOnGrid[0],
|
point(pointerOnGrid[0], pointerOnGrid[1]),
|
||||||
pointerOnGrid[1],
|
point(cx, cy),
|
||||||
cx,
|
-element.angle as Radians,
|
||||||
cy,
|
|
||||||
-element.angle,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [rotatedX - element.x, rotatedY - element.y];
|
return point(rotatedX - element.x, rotatedY - element.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1091,15 +1104,19 @@ export class LinearElementEditor {
|
||||||
* expected in various parts of the codebase. Also returns new x/y to account
|
* expected in various parts of the codebase. Also returns new x/y to account
|
||||||
* for the potential normalization.
|
* for the potential normalization.
|
||||||
*/
|
*/
|
||||||
static getNormalizedPoints(element: ExcalidrawLinearElement) {
|
static getNormalizedPoints(element: ExcalidrawLinearElement): {
|
||||||
|
points: LocalPoint[];
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
const offsetX = points[0][0];
|
const offsetX = points[0][0];
|
||||||
const offsetY = points[0][1];
|
const offsetY = points[0][1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
points: points.map((point) => {
|
points: points.map((p) => {
|
||||||
return [point[0] - offsetX, point[1] - offsetY] as const;
|
return point(p[0] - offsetX, p[1] - offsetY);
|
||||||
}),
|
}),
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
|
@ -1116,17 +1133,23 @@ export class LinearElementEditor {
|
||||||
static duplicateSelectedPoints(
|
static duplicateSelectedPoints(
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
) {
|
): AppState {
|
||||||
if (!appState.editingLinearElement) {
|
invariant(
|
||||||
return false;
|
appState.editingLinearElement,
|
||||||
}
|
"Not currently editing a linear element",
|
||||||
|
);
|
||||||
|
|
||||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
if (!element || selectedPointsIndices === null) {
|
invariant(
|
||||||
return false;
|
element,
|
||||||
}
|
"The linear element does not exist in the provided Scene",
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
selectedPointsIndices != null,
|
||||||
|
"There are no selected points to duplicate",
|
||||||
|
);
|
||||||
|
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
@ -1134,9 +1157,9 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
let pointAddedToEnd = false;
|
let pointAddedToEnd = false;
|
||||||
let indexCursor = -1;
|
let indexCursor = -1;
|
||||||
const nextPoints = points.reduce((acc: Point[], point, index) => {
|
const nextPoints = points.reduce((acc: LocalPoint[], p, index) => {
|
||||||
++indexCursor;
|
++indexCursor;
|
||||||
acc.push(point);
|
acc.push(p);
|
||||||
|
|
||||||
const isSelected = selectedPointsIndices.includes(index);
|
const isSelected = selectedPointsIndices.includes(index);
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
@ -1147,8 +1170,8 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
acc.push(
|
acc.push(
|
||||||
nextPoint
|
nextPoint
|
||||||
? [(point[0] + nextPoint[0]) / 2, (point[1] + nextPoint[1]) / 2]
|
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||||
: [point[0], point[1]],
|
: point(p[0], p[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
nextSelectedIndices.push(indexCursor + 1);
|
nextSelectedIndices.push(indexCursor + 1);
|
||||||
|
@ -1169,7 +1192,7 @@ export class LinearElementEditor {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: [lastPoint[0] + 30, lastPoint[1] + 30],
|
point: point(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -1177,12 +1200,10 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
...appState,
|
||||||
...appState,
|
editingLinearElement: {
|
||||||
editingLinearElement: {
|
...appState.editingLinearElement,
|
||||||
...appState.editingLinearElement,
|
selectedPointsIndices: nextSelectedIndices,
|
||||||
selectedPointsIndices: nextSelectedIndices,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1209,10 +1230,10 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
|
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
||||||
if (!pointIndices.includes(idx)) {
|
if (!pointIndices.includes(idx)) {
|
||||||
acc.push(
|
acc.push(
|
||||||
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
|
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -1229,7 +1250,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { point: Point }[],
|
targetPoints: { point: LocalPoint }[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
|
@ -1247,7 +1268,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
|
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
|
@ -1277,11 +1298,11 @@ export class LinearElementEditor {
|
||||||
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
|
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = points.map((point, idx) => {
|
const nextPoints: LocalPoint[] = points.map((p, idx) => {
|
||||||
const selectedPointData = targetPoints.find((p) => p.index === idx);
|
const selectedPointData = targetPoints.find((t) => t.index === idx);
|
||||||
if (selectedPointData) {
|
if (selectedPointData) {
|
||||||
if (selectedPointData.index === 0) {
|
if (selectedPointData.index === 0) {
|
||||||
return point;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deltaX =
|
const deltaX =
|
||||||
|
@ -1289,14 +1310,9 @@ export class LinearElementEditor {
|
||||||
const deltaY =
|
const deltaY =
|
||||||
selectedPointData.point[1] - points[selectedPointData.index][1];
|
selectedPointData.point[1] - points[selectedPointData.index][1];
|
||||||
|
|
||||||
return [
|
return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
||||||
point[0] + deltaX - offsetX,
|
|
||||||
point[1] + deltaY - offsetY,
|
|
||||||
] as const;
|
|
||||||
}
|
}
|
||||||
return offsetX || offsetY
|
return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p;
|
||||||
? ([point[0] - offsetX, point[1] - offsetY] as const)
|
|
||||||
: point;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
|
@ -1349,11 +1365,9 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const origin = linearElementEditor.pointerDownState.origin!;
|
const origin = linearElementEditor.pointerDownState.origin!;
|
||||||
const dist = distance2d(
|
const dist = pointDistance(
|
||||||
origin.x,
|
point(origin.x, origin.y),
|
||||||
origin.y,
|
point(pointerCoords.x, pointerCoords.y),
|
||||||
pointerCoords.x,
|
|
||||||
pointerCoords.y,
|
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
|
@ -1418,7 +1432,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
private static _updatePoints(
|
private static _updatePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
nextPoints: readonly Point[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
@ -1461,7 +1475,7 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
mergedElementsMap,
|
mergedElementsMap,
|
||||||
nextPoints,
|
nextPoints,
|
||||||
[offsetX, offsetY],
|
vector(offsetX, offsetY),
|
||||||
bindings,
|
bindings,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
@ -1474,7 +1488,11 @@ export class LinearElementEditor {
|
||||||
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
||||||
const dX = prevCenterX - nextCenterX;
|
const dX = prevCenterX - nextCenterX;
|
||||||
const dY = prevCenterY - nextCenterY;
|
const dY = prevCenterY - nextCenterY;
|
||||||
const rotated = rotate(offsetX, offsetY, dX, dY, element.angle);
|
const rotated = pointRotateRads(
|
||||||
|
point(offsetX, offsetY),
|
||||||
|
point(dX, dY),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
...otherUpdates,
|
...otherUpdates,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
|
@ -1487,8 +1505,8 @@ export class LinearElementEditor {
|
||||||
private static _getShiftLockedDelta(
|
private static _getShiftLockedDelta(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
referencePoint: Point,
|
referencePoint: LocalPoint,
|
||||||
scenePointer: Point,
|
scenePointer: GlobalPoint,
|
||||||
gridSize: NullableGridSize,
|
gridSize: NullableGridSize,
|
||||||
) {
|
) {
|
||||||
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
@ -1517,7 +1535,11 @@ export class LinearElementEditor {
|
||||||
gridY,
|
gridY,
|
||||||
);
|
);
|
||||||
|
|
||||||
return rotatePoint([width, height], [0, 0], -element.angle);
|
return pointRotateRads(
|
||||||
|
point(width, height),
|
||||||
|
point(0, 0),
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBoundTextElementPosition = (
|
static getBoundTextElementPosition = (
|
||||||
|
@ -1548,7 +1570,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
let midSegmentMidpoint = editorMidPointsCache.points[index];
|
let midSegmentMidpoint = editorMidPointsCache.points[index];
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
midSegmentMidpoint = centerPoint(points[0], points[1]);
|
midSegmentMidpoint = pointCenter(points[0], points[1]);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!midSegmentMidpoint ||
|
!midSegmentMidpoint ||
|
||||||
|
@ -1585,37 +1607,38 @@ export class LinearElementEditor {
|
||||||
);
|
);
|
||||||
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
||||||
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
||||||
|
const centerPoint = point(cx, cy);
|
||||||
|
|
||||||
const topLeftRotatedPoint = rotatePoint([x1, y1], [cx, cy], element.angle);
|
const topLeftRotatedPoint = pointRotateRads(
|
||||||
const topRightRotatedPoint = rotatePoint([x2, y1], [cx, cy], element.angle);
|
point(x1, y1),
|
||||||
|
centerPoint,
|
||||||
const counterRotateBoundTextTopLeft = rotatePoint(
|
element.angle,
|
||||||
[boundTextX1, boundTextY1],
|
|
||||||
|
|
||||||
[cx, cy],
|
|
||||||
|
|
||||||
-element.angle,
|
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextTopRight = rotatePoint(
|
const topRightRotatedPoint = pointRotateRads(
|
||||||
[boundTextX2, boundTextY1],
|
point(x2, y1),
|
||||||
|
centerPoint,
|
||||||
[cx, cy],
|
element.angle,
|
||||||
|
|
||||||
-element.angle,
|
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomLeft = rotatePoint(
|
|
||||||
[boundTextX1, boundTextY2],
|
|
||||||
|
|
||||||
[cx, cy],
|
const counterRotateBoundTextTopLeft = pointRotateRads(
|
||||||
|
point(boundTextX1, boundTextY1),
|
||||||
-element.angle,
|
centerPoint,
|
||||||
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
const counterRotateBoundTextBottomRight = rotatePoint(
|
const counterRotateBoundTextTopRight = pointRotateRads(
|
||||||
[boundTextX2, boundTextY2],
|
point(boundTextX2, boundTextY1),
|
||||||
|
centerPoint,
|
||||||
[cx, cy],
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
-element.angle,
|
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
||||||
|
point(boundTextX1, boundTextY2),
|
||||||
|
centerPoint,
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
const counterRotateBoundTextBottomRight = pointRotateRads(
|
||||||
|
point(boundTextX2, boundTextY2),
|
||||||
|
centerPoint,
|
||||||
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { ExcalidrawElement } from "./types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import type { Point } from "../types";
|
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
@ -59,8 +58,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
let didChangePoints = false;
|
let didChangePoints = false;
|
||||||
let index = prevPoints.length;
|
let index = prevPoints.length;
|
||||||
while (--index) {
|
while (--index) {
|
||||||
const prevPoint: Point = prevPoints[index];
|
const prevPoint = prevPoints[index];
|
||||||
const nextPoint: Point = nextPoints[index];
|
const nextPoint = nextPoints[index];
|
||||||
if (
|
if (
|
||||||
prevPoint[0] !== nextPoint[0] ||
|
prevPoint[0] !== nextPoint[0] ||
|
||||||
prevPoint[1] !== nextPoint[1]
|
prevPoint[1] !== nextPoint[1]
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { API } from "../tests/helpers/api";
|
||||||
import { FONT_FAMILY, ROUNDNESS } from "../constants";
|
import { FONT_FAMILY, ROUNDNESS } from "../constants";
|
||||||
import { isPrimitive } from "../utils";
|
import { isPrimitive } from "../utils";
|
||||||
import type { ExcalidrawLinearElement } from "./types";
|
import type { ExcalidrawLinearElement } from "./types";
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
const assertCloneObjects = (source: any, clone: any) => {
|
const assertCloneObjects = (source: any, clone: any) => {
|
||||||
for (const key in clone) {
|
for (const key in clone) {
|
||||||
|
@ -36,10 +38,7 @@ describe("duplicating single elements", () => {
|
||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [
|
points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)],
|
||||||
[1, 2],
|
|
||||||
[3, 4],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element);
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|
|
@ -30,7 +30,6 @@ import { bumpVersion, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { adjustXYWithRotation } from "../math";
|
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
measureText,
|
measureText,
|
||||||
|
@ -48,6 +47,7 @@ import {
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
|
import type { Radians } from "../../math";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
@ -88,7 +88,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
||||||
opacity = DEFAULT_ELEMENT_PROPS.opacity,
|
opacity = DEFAULT_ELEMENT_PROPS.opacity,
|
||||||
width = 0,
|
width = 0,
|
||||||
height = 0,
|
height = 0,
|
||||||
angle = 0,
|
angle = 0 as Radians,
|
||||||
groupIds = [],
|
groupIds = [],
|
||||||
frameId = null,
|
frameId = null,
|
||||||
index = null,
|
index = null,
|
||||||
|
@ -348,6 +348,53 @@ const getAdjustedDimensions = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const adjustXYWithRotation = (
|
||||||
|
sides: {
|
||||||
|
n?: boolean;
|
||||||
|
e?: boolean;
|
||||||
|
s?: boolean;
|
||||||
|
w?: boolean;
|
||||||
|
},
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
angle: number,
|
||||||
|
deltaX1: number,
|
||||||
|
deltaY1: number,
|
||||||
|
deltaX2: number,
|
||||||
|
deltaY2: number,
|
||||||
|
): [number, number] => {
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
if (sides.e && sides.w) {
|
||||||
|
x += deltaX1 + deltaX2;
|
||||||
|
} else if (sides.e) {
|
||||||
|
x += deltaX1 * (1 + cos);
|
||||||
|
y += deltaX1 * sin;
|
||||||
|
x += deltaX2 * (1 - cos);
|
||||||
|
y += deltaX2 * -sin;
|
||||||
|
} else if (sides.w) {
|
||||||
|
x += deltaX1 * (1 - cos);
|
||||||
|
y += deltaX1 * -sin;
|
||||||
|
x += deltaX2 * (1 + cos);
|
||||||
|
y += deltaX2 * sin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sides.n && sides.s) {
|
||||||
|
y += deltaY1 + deltaY2;
|
||||||
|
} else if (sides.n) {
|
||||||
|
x += deltaY1 * sin;
|
||||||
|
y += deltaY1 * (1 - cos);
|
||||||
|
x += deltaY2 * -sin;
|
||||||
|
y += deltaY2 * (1 + cos);
|
||||||
|
} else if (sides.s) {
|
||||||
|
x += deltaY1 * -sin;
|
||||||
|
y += deltaY1 * (1 + cos);
|
||||||
|
x += deltaY2 * sin;
|
||||||
|
y += deltaY2 * (1 - cos);
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
|
||||||
export const refreshTextDimensions = (
|
export const refreshTextDimensions = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawTextContainer | null,
|
container: ExcalidrawTextContainer | null,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, centerPoint, rotatePoint } from "../math";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
@ -38,7 +36,7 @@ import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import type { Point, PointerDownState } from "../types";
|
import type { PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
|
@ -55,16 +53,15 @@ import {
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { isInGroup } from "../groups";
|
import { isInGroup } from "../groups";
|
||||||
import { mutateElbowArrow } from "./routing";
|
import { mutateElbowArrow } from "./routing";
|
||||||
|
import type { GlobalPoint } from "../../math";
|
||||||
export const normalizeAngle = (angle: number): number => {
|
import {
|
||||||
if (angle < 0) {
|
pointCenter,
|
||||||
return angle + 2 * Math.PI;
|
normalizeRadians,
|
||||||
}
|
point,
|
||||||
if (angle >= 2 * Math.PI) {
|
pointFromPair,
|
||||||
return angle - 2 * Math.PI;
|
pointRotateRads,
|
||||||
}
|
type Radians,
|
||||||
return angle;
|
} from "../../math";
|
||||||
};
|
|
||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
||||||
|
@ -158,16 +155,17 @@ const rotateSingleElement = (
|
||||||
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;
|
||||||
let angle: number;
|
let angle: Radians;
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
angle = 0;
|
angle = 0 as Radians;
|
||||||
} else {
|
} else {
|
||||||
angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
angle = ((5 * Math.PI) / 2 +
|
||||||
|
Math.atan2(pointerY - cy, pointerX - cx)) as Radians;
|
||||||
if (shouldRotateWithDiscreteAngle) {
|
if (shouldRotateWithDiscreteAngle) {
|
||||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
angle = (angle + SHIFT_LOCKING_ANGLE / 2) as Radians;
|
||||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
angle = (angle - (angle % SHIFT_LOCKING_ANGLE)) as Radians;
|
||||||
}
|
}
|
||||||
angle = normalizeAngle(angle);
|
angle = normalizeRadians(angle as Radians);
|
||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
|
@ -240,12 +238,10 @@ const resizeSingleTextElement = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
// rotation pointer with reverse angle
|
// rotation pointer with reverse angle
|
||||||
const [rotatedX, rotatedY] = rotate(
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
pointerX,
|
point(pointerX, pointerY),
|
||||||
pointerY,
|
point(cx, cy),
|
||||||
cx,
|
-element.angle as Radians,
|
||||||
cy,
|
|
||||||
-element.angle,
|
|
||||||
);
|
);
|
||||||
let scaleX = 0;
|
let scaleX = 0;
|
||||||
let scaleY = 0;
|
let scaleY = 0;
|
||||||
|
@ -279,20 +275,26 @@ const resizeSingleTextElement = (
|
||||||
const startBottomRight = [x2, y2];
|
const startBottomRight = [x2, y2];
|
||||||
const startCenter = [cx, cy];
|
const startCenter = [cx, cy];
|
||||||
|
|
||||||
let newTopLeft = [x1, y1] as [number, number];
|
let newTopLeft = point<GlobalPoint>(x1, y1);
|
||||||
if (["n", "w", "nw"].includes(transformHandleType)) {
|
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||||
newTopLeft = [
|
newTopLeft = point<GlobalPoint>(
|
||||||
startBottomRight[0] - Math.abs(nextWidth),
|
startBottomRight[0] - Math.abs(nextWidth),
|
||||||
startBottomRight[1] - Math.abs(nextHeight),
|
startBottomRight[1] - Math.abs(nextHeight),
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
if (transformHandleType === "ne") {
|
if (transformHandleType === "ne") {
|
||||||
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||||
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(nextHeight)];
|
newTopLeft = point<GlobalPoint>(
|
||||||
|
bottomLeft[0],
|
||||||
|
bottomLeft[1] - Math.abs(nextHeight),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (transformHandleType === "sw") {
|
if (transformHandleType === "sw") {
|
||||||
const topRight = [startBottomRight[0], startTopLeft[1]];
|
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||||
newTopLeft = [topRight[0] - Math.abs(nextWidth), topRight[1]];
|
newTopLeft = point<GlobalPoint>(
|
||||||
|
topRight[0] - Math.abs(nextWidth),
|
||||||
|
topRight[1],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["s", "n"].includes(transformHandleType)) {
|
if (["s", "n"].includes(transformHandleType)) {
|
||||||
|
@ -308,13 +310,17 @@ const resizeSingleTextElement = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const angle = element.angle;
|
const angle = element.angle;
|
||||||
const rotatedTopLeft = rotatePoint(newTopLeft, [cx, cy], angle);
|
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
|
||||||
const newCenter: Point = [
|
const newCenter = point<GlobalPoint>(
|
||||||
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
||||||
];
|
);
|
||||||
const rotatedNewCenter = rotatePoint(newCenter, [cx, cy], angle);
|
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
|
||||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
newTopLeft = pointRotateRads(
|
||||||
|
rotatedTopLeft,
|
||||||
|
rotatedNewCenter,
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
const [nextX, nextY] = newTopLeft;
|
const [nextX, nextY] = newTopLeft;
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
|
@ -334,14 +340,14 @@ const resizeSingleTextElement = (
|
||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft: Point = [x1, y1];
|
const startTopLeft = point<GlobalPoint>(x1, y1);
|
||||||
const startBottomRight: Point = [x2, y2];
|
const startBottomRight = point<GlobalPoint>(x2, y2);
|
||||||
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
|
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
const rotatedPointer = rotatePoint(
|
const rotatedPointer = pointRotateRads(
|
||||||
[pointerX, pointerY],
|
point(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [esx1, , esx2] = getResizedElementAbsoluteCoords(
|
const [esx1, , esx2] = getResizedElementAbsoluteCoords(
|
||||||
|
@ -407,13 +413,21 @@ const resizeSingleTextElement = (
|
||||||
|
|
||||||
// adjust topLeft to new rotation point
|
// adjust topLeft to new rotation point
|
||||||
const angle = stateAtResizeStart.angle;
|
const angle = stateAtResizeStart.angle;
|
||||||
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
const rotatedTopLeft = pointRotateRads(
|
||||||
const newCenter: Point = [
|
pointFromPair(newTopLeft),
|
||||||
|
startCenter,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
const newCenter = point(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
];
|
);
|
||||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
|
||||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
newTopLeft = pointRotateRads(
|
||||||
|
rotatedTopLeft,
|
||||||
|
rotatedNewCenter,
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
const resizedElement: Partial<ExcalidrawTextElement> = {
|
const resizedElement: Partial<ExcalidrawTextElement> = {
|
||||||
width: Math.abs(newWidth),
|
width: Math.abs(newWidth),
|
||||||
|
@ -446,15 +460,15 @@ export const resizeSingleElement = (
|
||||||
stateAtResizeStart.height,
|
stateAtResizeStart.height,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const startTopLeft: Point = [x1, y1];
|
const startTopLeft = point(x1, y1);
|
||||||
const startBottomRight: Point = [x2, y2];
|
const startBottomRight = point(x2, y2);
|
||||||
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
|
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
// Calculate new dimensions based on cursor position
|
// Calculate new dimensions based on cursor position
|
||||||
const rotatedPointer = rotatePoint(
|
const rotatedPointer = pointRotateRads(
|
||||||
[pointerX, pointerY],
|
point(pointerX, pointerY),
|
||||||
startCenter,
|
startCenter,
|
||||||
-stateAtResizeStart.angle,
|
-stateAtResizeStart.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get bounds corners rendered on screen
|
// Get bounds corners rendered on screen
|
||||||
|
@ -628,13 +642,21 @@ export const resizeSingleElement = (
|
||||||
|
|
||||||
// adjust topLeft to new rotation point
|
// adjust topLeft to new rotation point
|
||||||
const angle = stateAtResizeStart.angle;
|
const angle = stateAtResizeStart.angle;
|
||||||
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
const rotatedTopLeft = pointRotateRads(
|
||||||
const newCenter: Point = [
|
pointFromPair(newTopLeft),
|
||||||
|
startCenter,
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
const newCenter = point(
|
||||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
];
|
);
|
||||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
|
||||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
newTopLeft = pointRotateRads(
|
||||||
|
rotatedTopLeft,
|
||||||
|
rotatedNewCenter,
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||||
// So we need to readjust (x,y) to be where the first point should be
|
// So we need to readjust (x,y) to be where the first point should be
|
||||||
|
@ -793,21 +815,21 @@ export const resizeMultipleElements = (
|
||||||
|
|
||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const anchorsMap: Record<TransformHandleDirection, Point> = {
|
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
||||||
ne: [minX, maxY],
|
ne: point(minX, maxY),
|
||||||
se: [minX, minY],
|
se: point(minX, minY),
|
||||||
sw: [maxX, minY],
|
sw: point(maxX, minY),
|
||||||
nw: [maxX, maxY],
|
nw: point(maxX, maxY),
|
||||||
e: [minX, minY + height / 2],
|
e: point(minX, minY + height / 2),
|
||||||
w: [maxX, minY + height / 2],
|
w: point(maxX, minY + height / 2),
|
||||||
n: [minX + width / 2, maxY],
|
n: point(minX + width / 2, maxY),
|
||||||
s: [minX + width / 2, minY],
|
s: point(minX + width / 2, minY),
|
||||||
};
|
};
|
||||||
|
|
||||||
// anchor point must be on the opposite side of the dragged selection handle
|
// anchor point must be on the opposite side of the dragged selection handle
|
||||||
// or be the center of the selection if shouldResizeFromCenter
|
// or be the center of the selection if shouldResizeFromCenter
|
||||||
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
const [anchorX, anchorY] = shouldResizeFromCenter
|
||||||
? [midX, midY]
|
? point(midX, midY)
|
||||||
: anchorsMap[direction];
|
: anchorsMap[direction];
|
||||||
|
|
||||||
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||||
|
@ -898,7 +920,9 @@ export const resizeMultipleElements = (
|
||||||
|
|
||||||
const width = orig.width * scaleX;
|
const width = orig.width * scaleX;
|
||||||
const height = orig.height * scaleY;
|
const height = orig.height * scaleY;
|
||||||
const angle = normalizeAngle(orig.angle * flipFactorX * flipFactorY);
|
const angle = normalizeRadians(
|
||||||
|
(orig.angle * flipFactorX * flipFactorY) as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
|
const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
|
||||||
const offsetX = orig.x - anchorX;
|
const offsetX = orig.x - anchorX;
|
||||||
|
@ -1029,12 +1053,10 @@ const rotateMultipleElements = (
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const origAngle =
|
const origAngle =
|
||||||
originalElements.get(element.id)?.angle ?? element.angle;
|
originalElements.get(element.id)?.angle ?? element.angle;
|
||||||
const [rotatedCX, rotatedCY] = rotate(
|
const [rotatedCX, rotatedCY] = pointRotateRads(
|
||||||
cx,
|
point(cx, cy),
|
||||||
cy,
|
point(centerX, centerY),
|
||||||
centerX,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
centerY,
|
|
||||||
centerAngle + origAngle - element.angle,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||||
|
@ -1046,7 +1068,7 @@ const rotateMultipleElements = (
|
||||||
{
|
{
|
||||||
x: element.x + (rotatedCX - cx),
|
x: element.x + (rotatedCX - cx),
|
||||||
y: element.y + (rotatedCY - cy),
|
y: element.y + (rotatedCY - cy),
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -1063,7 +1085,7 @@ const rotateMultipleElements = (
|
||||||
{
|
{
|
||||||
x: boundText.x + (rotatedCX - cx),
|
x: boundText.x + (rotatedCX - cx),
|
||||||
y: boundText.y + (rotatedCY - cy),
|
y: boundText.y + (rotatedCY - cy),
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -1086,25 +1108,43 @@ export const getResizeOffsetXY = (
|
||||||
: getCommonBounds(selectedElements);
|
: getCommonBounds(selectedElements);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const angle = selectedElements.length === 1 ? selectedElements[0].angle : 0;
|
const angle = (
|
||||||
[x, y] = rotate(x, y, cx, cy, -angle);
|
selectedElements.length === 1 ? selectedElements[0].angle : 0
|
||||||
|
) as Radians;
|
||||||
|
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
|
||||||
switch (transformHandleType) {
|
switch (transformHandleType) {
|
||||||
case "n":
|
case "n":
|
||||||
return rotate(x - (x1 + x2) / 2, y - y1, 0, 0, angle);
|
return pointRotateRads(
|
||||||
|
point(x - (x1 + x2) / 2, y - y1),
|
||||||
|
point(0, 0),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
case "s":
|
case "s":
|
||||||
return rotate(x - (x1 + x2) / 2, y - y2, 0, 0, angle);
|
return pointRotateRads(
|
||||||
|
point(x - (x1 + x2) / 2, y - y2),
|
||||||
|
point(0, 0),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
case "w":
|
case "w":
|
||||||
return rotate(x - x1, y - (y1 + y2) / 2, 0, 0, angle);
|
return pointRotateRads(
|
||||||
|
point(x - x1, y - (y1 + y2) / 2),
|
||||||
|
point(0, 0),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
case "e":
|
case "e":
|
||||||
return rotate(x - x2, y - (y1 + y2) / 2, 0, 0, angle);
|
return pointRotateRads(
|
||||||
|
point(x - x2, y - (y1 + y2) / 2),
|
||||||
|
point(0, 0),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
case "nw":
|
case "nw":
|
||||||
return rotate(x - x1, y - y1, 0, 0, angle);
|
return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle);
|
||||||
case "ne":
|
case "ne":
|
||||||
return rotate(x - x2, y - y1, 0, 0, angle);
|
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
|
||||||
case "sw":
|
case "sw":
|
||||||
return rotate(x - x1, y - y2, 0, 0, angle);
|
return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle);
|
||||||
case "se":
|
case "se":
|
||||||
return rotate(x - x2, y - y2, 0, 0, angle);
|
return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle);
|
||||||
default:
|
default:
|
||||||
return [0, 0];
|
return [0, 0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,14 @@ import type { AppState, Device, Zoom } from "../types";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
||||||
import {
|
|
||||||
angleToDegrees,
|
|
||||||
pointOnLine,
|
|
||||||
pointRotate,
|
|
||||||
} from "../../utils/geometry/geometry";
|
|
||||||
import type { Line, Point } from "../../utils/geometry/shape";
|
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
|
||||||
|
import {
|
||||||
|
point,
|
||||||
|
pointOnLineSegment,
|
||||||
|
pointRotateRads,
|
||||||
|
type Radians,
|
||||||
|
} from "../../math";
|
||||||
|
|
||||||
const isInsideTransformHandle = (
|
const isInsideTransformHandle = (
|
||||||
transformHandle: TransformHandle,
|
transformHandle: TransformHandle,
|
||||||
|
@ -38,7 +39,7 @@ const isInsideTransformHandle = (
|
||||||
y >= transformHandle[1] &&
|
y >= transformHandle[1] &&
|
||||||
y <= transformHandle[1] + transformHandle[3];
|
y <= transformHandle[1] + transformHandle[3];
|
||||||
|
|
||||||
export const resizeTest = (
|
export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
@ -91,15 +92,17 @@ export const resizeTest = (
|
||||||
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(
|
||||||
[x1 - SPACING, y1 - SPACING],
|
point(x1 - SPACING, y1 - SPACING),
|
||||||
[x2 + SPACING, y2 + SPACING],
|
point(x2 + SPACING, y2 + SPACING),
|
||||||
[cx, cy],
|
point(cx, cy),
|
||||||
angleToDegrees(element.angle),
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
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 (pointOnLine([x, y], side as Line, SPACING)) {
|
if (
|
||||||
|
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
||||||
|
) {
|
||||||
return dir as TransformHandleType;
|
return dir as TransformHandleType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +140,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,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
|
@ -147,7 +152,7 @@ export const getTransformHandleTypeFromCoords = (
|
||||||
): MaybeTransformHandleType => {
|
): MaybeTransformHandleType => {
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
||||||
0,
|
0 as Radians,
|
||||||
zoom,
|
zoom,
|
||||||
pointerType,
|
pointerType,
|
||||||
getOmitSidesForDevice(device),
|
getOmitSidesForDevice(device),
|
||||||
|
@ -173,15 +178,21 @@ export const getTransformHandleTypeFromCoords = (
|
||||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
|
|
||||||
const sides = getSelectionBorders(
|
const sides = getSelectionBorders(
|
||||||
[x1 - SPACING, y1 - SPACING],
|
point(x1 - SPACING, y1 - SPACING),
|
||||||
[x2 + SPACING, y2 + SPACING],
|
point(x2 + SPACING, y2 + SPACING),
|
||||||
[cx, cy],
|
point(cx, cy),
|
||||||
angleToDegrees(0),
|
0 as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
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 (pointOnLine([scenePointerX, scenePointerY], side as Line, SPACING)) {
|
if (
|
||||||
|
pointOnLineSegment(
|
||||||
|
point(scenePointerX, scenePointerY),
|
||||||
|
side as LineSegment<Point>,
|
||||||
|
SPACING,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return dir as TransformHandleType;
|
return dir as TransformHandleType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,16 +259,16 @@ export const getCursorForResizingElement = (resizingElement: {
|
||||||
return cursor ? `${cursor}-resize` : "";
|
return cursor ? `${cursor}-resize` : "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectionBorders = (
|
const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
|
||||||
[x1, y1]: Point,
|
[x1, y1]: Point,
|
||||||
[x2, y2]: Point,
|
[x2, y2]: Point,
|
||||||
center: Point,
|
center: Point,
|
||||||
angleInDegrees: number,
|
angle: Radians,
|
||||||
) => {
|
) => {
|
||||||
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
|
const topLeft = pointRotateRads(point(x1, y1), center, angle);
|
||||||
const topRight = pointRotate([x2, y1], angleInDegrees, center);
|
const topRight = pointRotateRads(point(x2, y1), center, angle);
|
||||||
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
|
const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
|
||||||
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
|
const bottomRight = pointRotateRads(point(x2, y2), center, angle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
n: [topLeft, topRight],
|
n: [topLeft, topRight],
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ARROW_TYPE } from "../constants";
|
import { ARROW_TYPE } from "../constants";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -31,8 +32,8 @@ describe("elbow arrow routing", () => {
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||||
[-45 - arrow.x, -100.1 - arrow.y],
|
point(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
[45 - arrow.x, 99.9 - arrow.y],
|
point(45 - arrow.x, 99.9 - arrow.y),
|
||||||
]);
|
]);
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
@ -68,10 +69,7 @@ describe("elbow arrow routing", () => {
|
||||||
y: -100.1,
|
y: -100.1,
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 200,
|
height: 200,
|
||||||
points: [
|
points: [point(0, 0), point(90, 200)],
|
||||||
[0, 0],
|
|
||||||
[90, 200],
|
|
||||||
],
|
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(rectangle1);
|
scene.insertElement(rectangle1);
|
||||||
scene.insertElement(rectangle2);
|
scene.insertElement(rectangle2);
|
||||||
|
@ -83,10 +81,7 @@ describe("elbow arrow routing", () => {
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElbowArrow(arrow, elementsMap, [
|
mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]);
|
||||||
[0, 0],
|
|
||||||
[90, 200],
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import { cross } from "../../utils/geometry/geometry";
|
import type { Radians } from "../../math";
|
||||||
import BinaryHeap from "../binaryheap";
|
|
||||||
import {
|
import {
|
||||||
aabbForElement,
|
point,
|
||||||
arePointsEqual,
|
pointScaleFromOrigin,
|
||||||
pointInsideBounds,
|
pointTranslate,
|
||||||
pointToVector,
|
vector,
|
||||||
scalePointFromOrigin,
|
vectorCross,
|
||||||
scaleVector,
|
vectorFromPoint,
|
||||||
translatePoint,
|
vectorScale,
|
||||||
} from "../math";
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
type Vector,
|
||||||
|
} from "../../math";
|
||||||
|
import BinaryHeap from "../binaryheap";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import type { Point } from "../types";
|
import { aabbForElement, pointInsideBounds } from "../shapes";
|
||||||
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
||||||
import {
|
import {
|
||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
|
@ -25,6 +28,8 @@ import {
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
import {
|
import {
|
||||||
|
compareHeading,
|
||||||
|
flipHeading,
|
||||||
HEADING_DOWN,
|
HEADING_DOWN,
|
||||||
HEADING_LEFT,
|
HEADING_LEFT,
|
||||||
HEADING_RIGHT,
|
HEADING_RIGHT,
|
||||||
|
@ -41,6 +46,8 @@ import type {
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { ElementsMap, ExcalidrawBindableElement } from "./types";
|
import type { ElementsMap, ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
|
|
||||||
type Node = {
|
type Node = {
|
||||||
f: number;
|
f: number;
|
||||||
g: number;
|
g: number;
|
||||||
|
@ -48,8 +55,8 @@ type Node = {
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
visited: boolean;
|
visited: boolean;
|
||||||
parent: Node | null;
|
parent: Node | null;
|
||||||
pos: Point;
|
pos: GlobalPoint;
|
||||||
addr: [number, number];
|
addr: GridAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Grid = {
|
type Grid = {
|
||||||
|
@ -63,8 +70,8 @@ const BASE_PADDING = 40;
|
||||||
export const mutateElbowArrow = (
|
export const mutateElbowArrow = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
nextPoints: readonly Point[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offset?: Point,
|
offset?: Vector,
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: FixedPointBinding | null;
|
startBinding?: FixedPointBinding | null;
|
||||||
endBinding?: FixedPointBinding | null;
|
endBinding?: FixedPointBinding | null;
|
||||||
|
@ -75,14 +82,20 @@ export const mutateElbowArrow = (
|
||||||
informMutation?: boolean;
|
informMutation?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const origStartGlobalPoint = translatePoint(nextPoints[0], [
|
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
||||||
arrow.x + (offset ? offset[0] : 0),
|
pointTranslate<LocalPoint, GlobalPoint>(
|
||||||
arrow.y + (offset ? offset[1] : 0),
|
nextPoints[0],
|
||||||
]);
|
vector(arrow.x, arrow.y),
|
||||||
const origEndGlobalPoint = translatePoint(nextPoints[nextPoints.length - 1], [
|
),
|
||||||
arrow.x + (offset ? offset[0] : 0),
|
offset,
|
||||||
arrow.y + (offset ? offset[1] : 0),
|
);
|
||||||
]);
|
const origEndGlobalPoint: GlobalPoint = pointTranslate(
|
||||||
|
pointTranslate<LocalPoint, GlobalPoint>(
|
||||||
|
nextPoints[nextPoints.length - 1],
|
||||||
|
vector(arrow.x, arrow.y),
|
||||||
|
),
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
const startElement =
|
const startElement =
|
||||||
arrow.startBinding &&
|
arrow.startBinding &&
|
||||||
|
@ -275,7 +288,10 @@ export const mutateElbowArrow = (
|
||||||
);
|
);
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
const points = path.map((node) => [node.pos[0], node.pos[1]]) as Point[];
|
const points = path.map((node) => [
|
||||||
|
node.pos[0],
|
||||||
|
node.pos[1],
|
||||||
|
]) as GlobalPoint[];
|
||||||
startDongle && points.unshift(startGlobalPoint);
|
startDongle && points.unshift(startGlobalPoint);
|
||||||
endDongle && points.push(endGlobalPoint);
|
endDongle && points.push(endGlobalPoint);
|
||||||
|
|
||||||
|
@ -284,7 +300,7 @@ export const mutateElbowArrow = (
|
||||||
{
|
{
|
||||||
...otherUpdates,
|
...otherUpdates,
|
||||||
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
|
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
|
||||||
angle: 0,
|
angle: 0 as Radians,
|
||||||
},
|
},
|
||||||
options?.informMutation,
|
options?.informMutation,
|
||||||
);
|
);
|
||||||
|
@ -363,7 +379,7 @@ const astar = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intersect
|
// Intersect
|
||||||
const neighborHalfPoint = scalePointFromOrigin(
|
const neighborHalfPoint = pointScaleFromOrigin(
|
||||||
neighbor.pos,
|
neighbor.pos,
|
||||||
current.pos,
|
current.pos,
|
||||||
0.5,
|
0.5,
|
||||||
|
@ -380,17 +396,17 @@ const astar = (
|
||||||
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
|
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
|
||||||
const neighborHeading = neighborIndexToHeading(i as 0 | 1 | 2 | 3);
|
const neighborHeading = neighborIndexToHeading(i as 0 | 1 | 2 | 3);
|
||||||
const previousDirection = current.parent
|
const previousDirection = current.parent
|
||||||
? vectorToHeading(pointToVector(current.pos, current.parent.pos))
|
? vectorToHeading(vectorFromPoint(current.pos, current.parent.pos))
|
||||||
: startHeading;
|
: startHeading;
|
||||||
|
|
||||||
// Do not allow going in reverse
|
// Do not allow going in reverse
|
||||||
const reverseHeading = scaleVector(previousDirection, -1);
|
const reverseHeading = flipHeading(previousDirection);
|
||||||
const neighborIsReverseRoute =
|
const neighborIsReverseRoute =
|
||||||
arePointsEqual(reverseHeading, neighborHeading) ||
|
compareHeading(reverseHeading, neighborHeading) ||
|
||||||
(arePointsEqual(start.addr, neighbor.addr) &&
|
(gridAddressesEqual(start.addr, neighbor.addr) &&
|
||||||
arePointsEqual(neighborHeading, startHeading)) ||
|
compareHeading(neighborHeading, startHeading)) ||
|
||||||
(arePointsEqual(end.addr, neighbor.addr) &&
|
(gridAddressesEqual(end.addr, neighbor.addr) &&
|
||||||
arePointsEqual(neighborHeading, endHeading));
|
compareHeading(neighborHeading, endHeading));
|
||||||
if (neighborIsReverseRoute) {
|
if (neighborIsReverseRoute) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -444,7 +460,7 @@ const pathTo = (start: Node, node: Node) => {
|
||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
const m_dist = (a: Point, b: Point) =>
|
const m_dist = (a: GlobalPoint | LocalPoint, b: GlobalPoint | LocalPoint) =>
|
||||||
Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -541,7 +557,12 @@ const generateDynamicAABBs = (
|
||||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||||
|
|
||||||
if (cross([a[2], a[1]], [a[0], a[3]], [endCenterX, endCenterY]) > 0) {
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vector(a[2] - endCenterX, a[1] - endCenterY),
|
||||||
|
vector(a[0] - endCenterX, a[3] - endCenterY),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
[first[0], first[1], cX, first[3]],
|
[first[0], first[1], cX, first[3]],
|
||||||
[cX, second[1], second[2], second[3]],
|
[cX, second[1], second[2], second[3]],
|
||||||
|
@ -557,7 +578,12 @@ const generateDynamicAABBs = (
|
||||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||||
|
|
||||||
if (cross([a[0], a[1]], [a[2], a[3]], [endCenterX, endCenterY]) > 0) {
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vector(a[0] - endCenterX, a[1] - endCenterY),
|
||||||
|
vector(a[2] - endCenterX, a[3] - endCenterY),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
[first[0], first[1], first[2], cY],
|
[first[0], first[1], first[2], cY],
|
||||||
[second[0], cY, second[2], second[3]],
|
[second[0], cY, second[2], second[3]],
|
||||||
|
@ -573,7 +599,12 @@ const generateDynamicAABBs = (
|
||||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||||
|
|
||||||
if (cross([a[2], a[1]], [a[0], a[3]], [endCenterX, endCenterY]) > 0) {
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vector(a[2] - endCenterX, a[1] - endCenterY),
|
||||||
|
vector(a[0] - endCenterX, a[3] - endCenterY),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
[cX, first[1], first[2], first[3]],
|
[cX, first[1], first[2], first[3]],
|
||||||
[second[0], second[1], cX, second[3]],
|
[second[0], second[1], cX, second[3]],
|
||||||
|
@ -589,7 +620,12 @@ const generateDynamicAABBs = (
|
||||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||||
|
|
||||||
if (cross([a[0], a[1]], [a[2], a[3]], [endCenterX, endCenterY]) > 0) {
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vector(a[0] - endCenterX, a[1] - endCenterY),
|
||||||
|
vector(a[2] - endCenterX, a[3] - endCenterY),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
[cX, first[1], first[2], first[3]],
|
[cX, first[1], first[2], first[3]],
|
||||||
[second[0], second[1], cX, second[3]],
|
[second[0], second[1], cX, second[3]],
|
||||||
|
@ -615,9 +651,9 @@ const generateDynamicAABBs = (
|
||||||
*/
|
*/
|
||||||
const calculateGrid = (
|
const calculateGrid = (
|
||||||
aabbs: Bounds[],
|
aabbs: Bounds[],
|
||||||
start: Point,
|
start: GlobalPoint,
|
||||||
startHeading: Heading,
|
startHeading: Heading,
|
||||||
end: Point,
|
end: GlobalPoint,
|
||||||
endHeading: Heading,
|
endHeading: Heading,
|
||||||
common: Bounds,
|
common: Bounds,
|
||||||
): Grid => {
|
): Grid => {
|
||||||
|
@ -662,8 +698,8 @@ const calculateGrid = (
|
||||||
closed: false,
|
closed: false,
|
||||||
visited: false,
|
visited: false,
|
||||||
parent: null,
|
parent: null,
|
||||||
addr: [col, row] as [number, number],
|
addr: [col, row] as GridAddress,
|
||||||
pos: [x, y] as Point,
|
pos: [x, y] as GlobalPoint,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -673,17 +709,17 @@ const calculateGrid = (
|
||||||
const getDonglePosition = (
|
const getDonglePosition = (
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
heading: Heading,
|
heading: Heading,
|
||||||
point: Point,
|
p: GlobalPoint,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
switch (heading) {
|
switch (heading) {
|
||||||
case HEADING_UP:
|
case HEADING_UP:
|
||||||
return [point[0], bounds[1]];
|
return point(p[0], bounds[1]);
|
||||||
case HEADING_RIGHT:
|
case HEADING_RIGHT:
|
||||||
return [bounds[2], point[1]];
|
return point(bounds[2], p[1]);
|
||||||
case HEADING_DOWN:
|
case HEADING_DOWN:
|
||||||
return [point[0], bounds[3]];
|
return point(p[0], bounds[3]);
|
||||||
}
|
}
|
||||||
return [bounds[0], point[1]];
|
return point(bounds[0], p[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const estimateSegmentCount = (
|
const estimateSegmentCount = (
|
||||||
|
@ -826,7 +862,7 @@ const gridNodeFromAddr = (
|
||||||
/**
|
/**
|
||||||
* Get node for global point on canvas (if exists)
|
* Get node for global point on canvas (if exists)
|
||||||
*/
|
*/
|
||||||
const pointToGridNode = (point: Point, grid: Grid): Node | null => {
|
const pointToGridNode = (point: GlobalPoint, grid: Grid): Node | null => {
|
||||||
for (let col = 0; col < grid.col; col++) {
|
for (let col = 0; col < grid.col; col++) {
|
||||||
for (let row = 0; row < grid.row; row++) {
|
for (let row = 0; row < grid.row; row++) {
|
||||||
const candidate = gridNodeFromAddr([col, row], grid);
|
const candidate = gridNodeFromAddr([col, row], grid);
|
||||||
|
@ -865,15 +901,24 @@ const getBindableElementForId = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizedArrowElementUpdate = (
|
const normalizedArrowElementUpdate = (
|
||||||
global: Point[],
|
global: GlobalPoint[],
|
||||||
externalOffsetX?: number,
|
externalOffsetX?: number,
|
||||||
externalOffsetY?: number,
|
externalOffsetY?: number,
|
||||||
) => {
|
): {
|
||||||
|
points: LocalPoint[];
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} => {
|
||||||
const offsetX = global[0][0];
|
const offsetX = global[0][0];
|
||||||
const offsetY = global[0][1];
|
const offsetY = global[0][1];
|
||||||
|
|
||||||
const points = global.map(
|
const points = global.map((p) =>
|
||||||
(point) => [point[0] - offsetX, point[1] - offsetY] as const,
|
pointTranslate<GlobalPoint, LocalPoint>(
|
||||||
|
p,
|
||||||
|
vectorScale(vectorFromPoint(global[0]), -1),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -885,19 +930,22 @@ const normalizedArrowElementUpdate = (
|
||||||
};
|
};
|
||||||
|
|
||||||
/// If last and current segments have the same heading, skip the middle point
|
/// If last and current segments have the same heading, skip the middle point
|
||||||
const simplifyElbowArrowPoints = (points: Point[]): Point[] =>
|
const simplifyElbowArrowPoints = (points: GlobalPoint[]): GlobalPoint[] =>
|
||||||
points
|
points
|
||||||
.slice(2)
|
.slice(2)
|
||||||
.reduce(
|
.reduce(
|
||||||
(result, point) =>
|
(result, p) =>
|
||||||
arePointsEqual(
|
compareHeading(
|
||||||
vectorToHeading(
|
vectorToHeading(
|
||||||
pointToVector(result[result.length - 1], result[result.length - 2]),
|
vectorFromPoint(
|
||||||
|
result[result.length - 1],
|
||||||
|
result[result.length - 2],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
vectorToHeading(pointToVector(point, result[result.length - 1])),
|
vectorToHeading(vectorFromPoint(p, result[result.length - 1])),
|
||||||
)
|
)
|
||||||
? [...result.slice(0, -1), point]
|
? [...result.slice(0, -1), p]
|
||||||
: [...result, point],
|
: [...result, p],
|
||||||
[points[0] ?? [0, 0], points[1] ?? [1, 0]],
|
[points[0] ?? [0, 0], points[1] ?? [1, 0]],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -915,13 +963,13 @@ const neighborIndexToHeading = (idx: number): Heading => {
|
||||||
|
|
||||||
const getGlobalPoint = (
|
const getGlobalPoint = (
|
||||||
fixedPointRatio: [number, number] | undefined | null,
|
fixedPointRatio: [number, number] | undefined | null,
|
||||||
initialPoint: Point,
|
initialPoint: GlobalPoint,
|
||||||
otherPoint: Point,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
boundElement?: ExcalidrawBindableElement | null,
|
boundElement?: ExcalidrawBindableElement | null,
|
||||||
hoveredElement?: ExcalidrawBindableElement | null,
|
hoveredElement?: ExcalidrawBindableElement | null,
|
||||||
isDragging?: boolean,
|
isDragging?: boolean,
|
||||||
): Point => {
|
): GlobalPoint => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
const snapPoint = getSnapPoint(
|
const snapPoint = getSnapPoint(
|
||||||
|
@ -956,36 +1004,34 @@ const getGlobalPoint = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSnapPoint = (
|
const getSnapPoint = (
|
||||||
point: Point,
|
p: GlobalPoint,
|
||||||
otherPoint: Point,
|
otherPoint: GlobalPoint,
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) =>
|
) =>
|
||||||
bindPointToSnapToElementOutline(
|
bindPointToSnapToElementOutline(
|
||||||
isRectanguloidElement(element)
|
isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p,
|
||||||
? avoidRectangularCorner(element, point)
|
|
||||||
: point,
|
|
||||||
otherPoint,
|
otherPoint,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
point: Point,
|
p: GlobalPoint,
|
||||||
otherPoint: Point,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: Point,
|
origPoint: GlobalPoint,
|
||||||
) =>
|
) =>
|
||||||
getHeadingForElbowArrowSnap(
|
getHeadingForElbowArrowSnap(
|
||||||
point,
|
p,
|
||||||
otherPoint,
|
otherPoint,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
hoveredElement &&
|
hoveredElement &&
|
||||||
aabbForElement(
|
aabbForElement(
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
Array(4).fill(
|
Array(4).fill(
|
||||||
distanceToBindableElement(hoveredElement, point, elementsMap),
|
distanceToBindableElement(hoveredElement, p, elementsMap),
|
||||||
) as [number, number, number, number],
|
) as [number, number, number, number],
|
||||||
),
|
),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -993,8 +1039,8 @@ const getBindPointHeading = (
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHoveredElements = (
|
const getHoveredElements = (
|
||||||
origStartGlobalPoint: Point,
|
origStartGlobalPoint: GlobalPoint,
|
||||||
origEndGlobalPoint: Point,
|
origEndGlobalPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
) => {
|
) => {
|
||||||
// TODO: Might be a performance bottleneck and the Map type
|
// TODO: Might be a performance bottleneck and the Map type
|
||||||
|
@ -1018,3 +1064,6 @@ const getHoveredElements = (
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
||||||
|
a[0] === b[0] && a[1] === b[1];
|
||||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
@ -41,10 +42,7 @@ describe("textWysiwyg", () => {
|
||||||
type: "line",
|
type: "line",
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
points: [
|
points: [point(0, 0), point(100, 0)],
|
||||||
[0, 0],
|
|
||||||
[100, 0],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const textSize = 20;
|
const textSize = 20;
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type {
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
|
||||||
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
@ -19,6 +18,8 @@ import {
|
||||||
isAndroid,
|
isAndroid,
|
||||||
isIOS,
|
isIOS,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
|
import type { Radians } from "../../math";
|
||||||
|
import { point, pointRotateRads } from "../../math";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
|
@ -91,9 +92,13 @@ const generateTransformHandle = (
|
||||||
height: number,
|
height: number,
|
||||||
cx: number,
|
cx: number,
|
||||||
cy: number,
|
cy: number,
|
||||||
angle: number,
|
angle: Radians,
|
||||||
): TransformHandle => {
|
): TransformHandle => {
|
||||||
const [xx, yy] = rotate(x + width / 2, y + height / 2, cx, cy, angle);
|
const [xx, yy] = pointRotateRads(
|
||||||
|
point(x + width / 2, y + height / 2),
|
||||||
|
point(cx, cy),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
return [xx - width / 2, yy - height / 2, width, height];
|
return [xx - width / 2, yy - height / 2, width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -119,7 +124,7 @@ export const getOmitSidesForDevice = (device: Device) => {
|
||||||
|
|
||||||
export const getTransformHandlesFromCoords = (
|
export const getTransformHandlesFromCoords = (
|
||||||
[x1, y1, x2, y2, cx, cy]: [number, number, number, number, number, number],
|
[x1, y1, x2, y2, cx, cy]: [number, number, number, number, number, number],
|
||||||
angle: number,
|
angle: Radians,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
omitSides: { [T in TransformHandleType]?: boolean } = {},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { LineSegment } from "../../utils";
|
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import type { ElementOrToolType, Point } from "../types";
|
import type { ElementOrToolType } from "../types";
|
||||||
import type { MarkNonNullable } from "../utility-types";
|
import type { MarkNonNullable } from "../utility-types";
|
||||||
import { assertNever } from "../utils";
|
import { assertNever } from "../utils";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
|
@ -191,7 +190,8 @@ export const isRectangularElement = (
|
||||||
element.type === "iframe" ||
|
element.type === "iframe" ||
|
||||||
element.type === "embeddable" ||
|
element.type === "embeddable" ||
|
||||||
element.type === "frame" ||
|
element.type === "frame" ||
|
||||||
element.type === "magicframe")
|
element.type === "magicframe" ||
|
||||||
|
element.type === "freedraw")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -325,10 +325,6 @@ export const isFixedPointBinding = (
|
||||||
return binding.fixedPoint != null;
|
return binding.fixedPoint != null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
|
||||||
export const isPoint = (point: unknown): point is Point =>
|
|
||||||
Array.isArray(point) && point.length === 2;
|
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
// TODO: Move this to @excalidraw/math
|
||||||
export const isBounds = (box: unknown): box is Bounds =>
|
export const isBounds = (box: unknown): box is Bounds =>
|
||||||
Array.isArray(box) &&
|
Array.isArray(box) &&
|
||||||
|
@ -337,10 +333,3 @@ export const isBounds = (box: unknown): box is Bounds =>
|
||||||
typeof box[1] === "number" &&
|
typeof box[1] === "number" &&
|
||||||
typeof box[2] === "number" &&
|
typeof box[2] === "number" &&
|
||||||
typeof box[3] === "number";
|
typeof box[3] === "number";
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
|
||||||
export const isLineSegment = (segment: unknown): segment is LineSegment =>
|
|
||||||
Array.isArray(segment) &&
|
|
||||||
segment.length === 2 &&
|
|
||||||
isPoint(segment[0]) &&
|
|
||||||
isPoint(segment[0]);
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Point } from "../types";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import type {
|
import type {
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
|
@ -49,7 +49,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||||
opacity: number;
|
opacity: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
angle: number;
|
angle: Radians;
|
||||||
/** Random integer used to seed shape generation so that the roughjs shape
|
/** Random integer used to seed shape generation so that the roughjs shape
|
||||||
doesn't differ across renders. */
|
doesn't differ across renders. */
|
||||||
seed: number;
|
seed: number;
|
||||||
|
@ -175,6 +175,15 @@ export type ExcalidrawFlowchartNodeElement =
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement;
|
| ExcalidrawEllipseElement;
|
||||||
|
|
||||||
|
export type ExcalidrawRectanguloidElement =
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawFreeDrawElement
|
||||||
|
| ExcalidrawIframeLikeElement
|
||||||
|
| ExcalidrawFrameLikeElement
|
||||||
|
| ExcalidrawEmbeddableElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
* no computed data. The list of all ExcalidrawElements should be shareable
|
* no computed data. The list of all ExcalidrawElements should be shareable
|
||||||
|
@ -283,8 +292,8 @@ export type Arrowhead =
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "line" | "arrow";
|
type: "line" | "arrow";
|
||||||
points: readonly Point[];
|
points: readonly LocalPoint[];
|
||||||
lastCommittedPoint: Point | null;
|
lastCommittedPoint: LocalPoint | null;
|
||||||
startBinding: PointBinding | null;
|
startBinding: PointBinding | null;
|
||||||
endBinding: PointBinding | null;
|
endBinding: PointBinding | null;
|
||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
|
@ -309,10 +318,10 @@ export type ExcalidrawElbowArrowElement = Merge<
|
||||||
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "freedraw";
|
type: "freedraw";
|
||||||
points: readonly Point[];
|
points: readonly LocalPoint[];
|
||||||
pressures: readonly number[];
|
pressures: readonly number[];
|
||||||
simulatePressure: boolean;
|
simulatePressure: boolean;
|
||||||
lastCommittedPoint: Point | null;
|
lastCommittedPoint: LocalPoint | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type FileId = string & { _brand: "FileId" };
|
export type FileId = string & { _brand: "FileId" };
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { isPointWithinBounds } from "./math";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
@ -30,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 { isPointWithinBounds, point } from "../math";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
|
@ -159,9 +159,9 @@ export const isCursorInFrame = (
|
||||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
|
||||||
|
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(
|
||||||
[fx1, fy1],
|
point(fx1, fy1),
|
||||||
[cursorCoords.x, cursorCoords.y],
|
point(cursorCoords.x, cursorCoords.y),
|
||||||
[fx2, fy2],
|
point(fx2, fy2),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
import {
|
|
||||||
isPointOnSymmetricArc,
|
|
||||||
rangeIntersection,
|
|
||||||
rangesOverlap,
|
|
||||||
rotate,
|
|
||||||
} from "./math";
|
|
||||||
|
|
||||||
describe("rotate", () => {
|
|
||||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
|
||||||
const x1 = 10;
|
|
||||||
const y1 = 20;
|
|
||||||
const x2 = 20;
|
|
||||||
const y2 = 30;
|
|
||||||
const angle = Math.PI / 2;
|
|
||||||
const [rotatedX, rotatedY] = rotate(x1, y1, x2, y2, angle);
|
|
||||||
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
|
||||||
const res2 = rotate(rotatedX, rotatedY, x2, y2, -angle);
|
|
||||||
expect(res2).toEqual([x1, x2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("range overlap", () => {
|
|
||||||
it("should overlap when range a contains range b", () => {
|
|
||||||
expect(rangesOverlap([1, 4], [2, 3])).toBe(true);
|
|
||||||
expect(rangesOverlap([1, 4], [1, 4])).toBe(true);
|
|
||||||
expect(rangesOverlap([1, 4], [1, 3])).toBe(true);
|
|
||||||
expect(rangesOverlap([1, 4], [2, 4])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should overlap when range b contains range a", () => {
|
|
||||||
expect(rangesOverlap([2, 3], [1, 4])).toBe(true);
|
|
||||||
expect(rangesOverlap([1, 3], [1, 4])).toBe(true);
|
|
||||||
expect(rangesOverlap([2, 4], [1, 4])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should overlap when range a and b intersect", () => {
|
|
||||||
expect(rangesOverlap([1, 4], [2, 5])).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("range intersection", () => {
|
|
||||||
it("should intersect completely with itself", () => {
|
|
||||||
expect(rangeIntersection([1, 4], [1, 4])).toEqual([1, 4]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should intersect irrespective of order", () => {
|
|
||||||
expect(rangeIntersection([1, 4], [2, 3])).toEqual([2, 3]);
|
|
||||||
expect(rangeIntersection([2, 3], [1, 4])).toEqual([2, 3]);
|
|
||||||
expect(rangeIntersection([1, 4], [3, 5])).toEqual([3, 4]);
|
|
||||||
expect(rangeIntersection([3, 5], [1, 4])).toEqual([3, 4]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should intersect at the edge", () => {
|
|
||||||
expect(rangeIntersection([1, 4], [4, 5])).toEqual([4, 4]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not intersect", () => {
|
|
||||||
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("point on arc", () => {
|
|
||||||
it("should detect point on simple arc", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
[0.92291667, 0.385],
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
it("should not detect point outside of a simple arc", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
[-0.92291667, 0.385],
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
it("should not detect point with good angle but incorrect radius", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
[-0.5, 0.5],
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,715 +0,0 @@
|
||||||
import type {
|
|
||||||
NormalizedZoomValue,
|
|
||||||
NullableGridSize,
|
|
||||||
Point,
|
|
||||||
Zoom,
|
|
||||||
} from "./types";
|
|
||||||
import {
|
|
||||||
DEFAULT_ADAPTIVE_RADIUS,
|
|
||||||
LINE_CONFIRM_THRESHOLD,
|
|
||||||
DEFAULT_PROPORTIONAL_RADIUS,
|
|
||||||
ROUNDNESS,
|
|
||||||
} from "./constants";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
NonDeleted,
|
|
||||||
} from "./element/types";
|
|
||||||
import type { Bounds } from "./element/bounds";
|
|
||||||
import { getCurvePathOps } from "./element/bounds";
|
|
||||||
import type { Mutable } from "./utility-types";
|
|
||||||
import { ShapeCache } from "./scene/ShapeCache";
|
|
||||||
import type { Vector } from "../utils/geometry/shape";
|
|
||||||
|
|
||||||
export const rotate = (
|
|
||||||
// target point to rotate
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
// point to rotate against
|
|
||||||
cx: number,
|
|
||||||
cy: number,
|
|
||||||
angle: number,
|
|
||||||
): [number, number] =>
|
|
||||||
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
|
||||||
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
|
||||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
|
||||||
[
|
|
||||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
|
||||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const rotatePoint = (
|
|
||||||
point: Point,
|
|
||||||
center: Point,
|
|
||||||
angle: number,
|
|
||||||
): [number, number] => rotate(point[0], point[1], center[0], center[1], angle);
|
|
||||||
|
|
||||||
export const adjustXYWithRotation = (
|
|
||||||
sides: {
|
|
||||||
n?: boolean;
|
|
||||||
e?: boolean;
|
|
||||||
s?: boolean;
|
|
||||||
w?: boolean;
|
|
||||||
},
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
angle: number,
|
|
||||||
deltaX1: number,
|
|
||||||
deltaY1: number,
|
|
||||||
deltaX2: number,
|
|
||||||
deltaY2: number,
|
|
||||||
): [number, number] => {
|
|
||||||
const cos = Math.cos(angle);
|
|
||||||
const sin = Math.sin(angle);
|
|
||||||
if (sides.e && sides.w) {
|
|
||||||
x += deltaX1 + deltaX2;
|
|
||||||
} else if (sides.e) {
|
|
||||||
x += deltaX1 * (1 + cos);
|
|
||||||
y += deltaX1 * sin;
|
|
||||||
x += deltaX2 * (1 - cos);
|
|
||||||
y += deltaX2 * -sin;
|
|
||||||
} else if (sides.w) {
|
|
||||||
x += deltaX1 * (1 - cos);
|
|
||||||
y += deltaX1 * -sin;
|
|
||||||
x += deltaX2 * (1 + cos);
|
|
||||||
y += deltaX2 * sin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sides.n && sides.s) {
|
|
||||||
y += deltaY1 + deltaY2;
|
|
||||||
} else if (sides.n) {
|
|
||||||
x += deltaY1 * sin;
|
|
||||||
y += deltaY1 * (1 - cos);
|
|
||||||
x += deltaY2 * -sin;
|
|
||||||
y += deltaY2 * (1 + cos);
|
|
||||||
} else if (sides.s) {
|
|
||||||
x += deltaY1 * -sin;
|
|
||||||
y += deltaY1 * (1 + cos);
|
|
||||||
x += deltaY2 * sin;
|
|
||||||
y += deltaY2 * (1 - cos);
|
|
||||||
}
|
|
||||||
return [x, y];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPointOnAPath = (point: Point, path: Point[]) => {
|
|
||||||
const [px, py] = point;
|
|
||||||
const [start, ...other] = path;
|
|
||||||
let [lastX, lastY] = start;
|
|
||||||
let kLine: number = 0;
|
|
||||||
let idx: number = 0;
|
|
||||||
|
|
||||||
// if any item in the array is true, it means that a point is
|
|
||||||
// on some segment of a line based path
|
|
||||||
const retVal = other.some(([x2, y2], i) => {
|
|
||||||
// we always take a line when dealing with line segments
|
|
||||||
const x1 = lastX;
|
|
||||||
const y1 = lastY;
|
|
||||||
|
|
||||||
lastX = x2;
|
|
||||||
lastY = y2;
|
|
||||||
|
|
||||||
// if a point is not within the domain of the line segment
|
|
||||||
// it is not on the line segment
|
|
||||||
if (px < x1 || px > x2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if all points lie on the same line
|
|
||||||
// y1 = kx1 + b, y2 = kx2 + b
|
|
||||||
// y2 - y1 = k(x2 - x2) -> k = (y2 - y1) / (x2 - x1)
|
|
||||||
|
|
||||||
// coefficient for the line (p0, p1)
|
|
||||||
const kL = (y2 - y1) / (x2 - x1);
|
|
||||||
|
|
||||||
// coefficient for the line segment (p0, point)
|
|
||||||
const kP1 = (py - y1) / (px - x1);
|
|
||||||
|
|
||||||
// coefficient for the line segment (point, p1)
|
|
||||||
const kP2 = (py - y2) / (px - x2);
|
|
||||||
|
|
||||||
// because we are basing both lines from the same starting point
|
|
||||||
// the only option for collinearity is having same coefficients
|
|
||||||
|
|
||||||
// using it for floating point comparisons
|
|
||||||
const epsilon = 0.3;
|
|
||||||
|
|
||||||
// if coefficient is more than an arbitrary epsilon,
|
|
||||||
// these lines are nor collinear
|
|
||||||
if (Math.abs(kP1 - kL) > epsilon && Math.abs(kP2 - kL) > epsilon) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the coefficient because we are goint to need it
|
|
||||||
kLine = kL;
|
|
||||||
idx = i;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return a coordinate that is always on the line segment
|
|
||||||
if (retVal === true) {
|
|
||||||
return { x: point[0], y: kLine * point[0], segment: idx };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
|
|
||||||
const xd = x2 - x1;
|
|
||||||
const yd = y2 - y1;
|
|
||||||
return Math.hypot(xd, yd);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const distanceSq2d = (p1: Point, p2: Point) => {
|
|
||||||
const xd = p2[0] - p1[0];
|
|
||||||
const yd = p2[1] - p1[1];
|
|
||||||
return xd * xd + yd * yd;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const centerPoint = (a: Point, b: Point): Point => {
|
|
||||||
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Checks if the first and last point are close enough
|
|
||||||
// to be considered a loop
|
|
||||||
export const isPathALoop = (
|
|
||||||
points: ExcalidrawLinearElement["points"],
|
|
||||||
/** supply if you want the loop detection to account for current zoom */
|
|
||||||
zoomValue: Zoom["value"] = 1 as NormalizedZoomValue,
|
|
||||||
): boolean => {
|
|
||||||
if (points.length >= 3) {
|
|
||||||
const [first, last] = [points[0], points[points.length - 1]];
|
|
||||||
const distance = distance2d(first[0], first[1], last[0], last[1]);
|
|
||||||
|
|
||||||
// Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in
|
|
||||||
// really close we make the threshold smaller, and vice versa.
|
|
||||||
return distance <= LINE_CONFIRM_THRESHOLD / zoomValue;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw a line from the point to the right till infiinty
|
|
||||||
// Check how many lines of the polygon does this infinite line intersects with
|
|
||||||
// If the number of intersections is odd, point is in the polygon
|
|
||||||
export const isPointInPolygon = (
|
|
||||||
points: Point[],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): boolean => {
|
|
||||||
const vertices = points.length;
|
|
||||||
|
|
||||||
// There must be at least 3 vertices in polygon
|
|
||||||
if (vertices < 3) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const extreme: Point = [Number.MAX_SAFE_INTEGER, y];
|
|
||||||
const p: Point = [x, y];
|
|
||||||
let count = 0;
|
|
||||||
for (let i = 0; i < vertices; i++) {
|
|
||||||
const current = points[i];
|
|
||||||
const next = points[(i + 1) % vertices];
|
|
||||||
if (doSegmentsIntersect(current, next, p, extreme)) {
|
|
||||||
if (orderedColinearOrientation(current, p, next) === 0) {
|
|
||||||
return isPointWithinBounds(current, p, next);
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// true if count is off
|
|
||||||
return count % 2 === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns whether `q` lies inside the segment/rectangle defined by `p` and `r`.
|
|
||||||
// This is an approximation to "does `q` lie on a segment `pr`" check.
|
|
||||||
export const isPointWithinBounds = (p: Point, q: Point, r: Point) => {
|
|
||||||
return (
|
|
||||||
q[0] <= Math.max(p[0], r[0]) &&
|
|
||||||
q[0] >= Math.min(p[0], r[0]) &&
|
|
||||||
q[1] <= Math.max(p[1], r[1]) &&
|
|
||||||
q[1] >= Math.min(p[1], r[1])
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For the ordered points p, q, r, return
|
|
||||||
// 0 if p, q, r are colinear
|
|
||||||
// 1 if Clockwise
|
|
||||||
// 2 if counterclickwise
|
|
||||||
const orderedColinearOrientation = (p: Point, q: Point, r: Point) => {
|
|
||||||
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
|
|
||||||
if (val === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return val > 0 ? 1 : 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check is p1q1 intersects with p2q2
|
|
||||||
const doSegmentsIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
|
|
||||||
const o1 = orderedColinearOrientation(p1, q1, p2);
|
|
||||||
const o2 = orderedColinearOrientation(p1, q1, q2);
|
|
||||||
const o3 = orderedColinearOrientation(p2, q2, p1);
|
|
||||||
const o4 = orderedColinearOrientation(p2, q2, q1);
|
|
||||||
|
|
||||||
if (o1 !== o2 && o3 !== o4) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1, q1 and p2 are colinear and p2 lies on segment p1q1
|
|
||||||
if (o1 === 0 && isPointWithinBounds(p1, p2, q1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1, q1 and p2 are colinear and q2 lies on segment p1q1
|
|
||||||
if (o2 === 0 && isPointWithinBounds(p1, q2, q1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// p2, q2 and p1 are colinear and p1 lies on segment p2q2
|
|
||||||
if (o3 === 0 && isPointWithinBounds(p2, p1, q2)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// p2, q2 and q1 are colinear and q1 lies on segment p2q2
|
|
||||||
if (o4 === 0 && isPointWithinBounds(p2, q1, q2)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Rounding this point causes some shake when free drawing
|
|
||||||
export const getGridPoint = (
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
gridSize: NullableGridSize,
|
|
||||||
): [number, number] => {
|
|
||||||
if (gridSize) {
|
|
||||||
return [
|
|
||||||
Math.round(x / gridSize) * gridSize,
|
|
||||||
Math.round(y / gridSize) * gridSize,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [x, y];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
|
||||||
if (
|
|
||||||
element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS ||
|
|
||||||
element.roundness?.type === ROUNDNESS.LEGACY
|
|
||||||
) {
|
|
||||||
return x * DEFAULT_PROPORTIONAL_RADIUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) {
|
|
||||||
const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS;
|
|
||||||
|
|
||||||
const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS;
|
|
||||||
|
|
||||||
if (x <= CUTOFF_SIZE) {
|
|
||||||
return x * DEFAULT_PROPORTIONAL_RADIUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixedRadiusSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getControlPointsForBezierCurve = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
endPoint: Point,
|
|
||||||
) => {
|
|
||||||
const shape = ShapeCache.generateElementShape(element, null);
|
|
||||||
if (!shape) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
|
||||||
let currentP: Mutable<Point> = [0, 0];
|
|
||||||
let index = 0;
|
|
||||||
let minDistance = Infinity;
|
|
||||||
let controlPoints: Mutable<Point>[] | null = null;
|
|
||||||
|
|
||||||
while (index < ops.length) {
|
|
||||||
const { op, data } = ops[index];
|
|
||||||
if (op === "move") {
|
|
||||||
currentP = data as unknown as Mutable<Point>;
|
|
||||||
}
|
|
||||||
if (op === "bcurveTo") {
|
|
||||||
const p0 = currentP;
|
|
||||||
const p1 = [data[0], data[1]] as Mutable<Point>;
|
|
||||||
const p2 = [data[2], data[3]] as Mutable<Point>;
|
|
||||||
const p3 = [data[4], data[5]] as Mutable<Point>;
|
|
||||||
const distance = distance2d(p3[0], p3[1], endPoint[0], endPoint[1]);
|
|
||||||
if (distance < minDistance) {
|
|
||||||
minDistance = distance;
|
|
||||||
controlPoints = [p0, p1, p2, p3];
|
|
||||||
}
|
|
||||||
currentP = p3;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return controlPoints;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBezierXY = (
|
|
||||||
p0: Point,
|
|
||||||
p1: Point,
|
|
||||||
p2: Point,
|
|
||||||
p3: Point,
|
|
||||||
t: number,
|
|
||||||
) => {
|
|
||||||
const equation = (t: number, idx: number) =>
|
|
||||||
Math.pow(1 - t, 3) * p3[idx] +
|
|
||||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
|
||||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
|
||||||
p0[idx] * Math.pow(t, 3);
|
|
||||||
const tx = equation(t, 0);
|
|
||||||
const ty = equation(t, 1);
|
|
||||||
return [tx, ty];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPointsInBezierCurve = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
endPoint: Point,
|
|
||||||
) => {
|
|
||||||
const controlPoints: Mutable<Point>[] = getControlPointsForBezierCurve(
|
|
||||||
element,
|
|
||||||
endPoint,
|
|
||||||
)!;
|
|
||||||
if (!controlPoints) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const pointsOnCurve: Mutable<Point>[] = [];
|
|
||||||
let t = 1;
|
|
||||||
// Take 20 points on curve for better accuracy
|
|
||||||
while (t > 0) {
|
|
||||||
const point = getBezierXY(
|
|
||||||
controlPoints[0],
|
|
||||||
controlPoints[1],
|
|
||||||
controlPoints[2],
|
|
||||||
controlPoints[3],
|
|
||||||
t,
|
|
||||||
);
|
|
||||||
pointsOnCurve.push([point[0], point[1]]);
|
|
||||||
t -= 0.05;
|
|
||||||
}
|
|
||||||
if (pointsOnCurve.length) {
|
|
||||||
if (arePointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
|
||||||
pointsOnCurve.push([endPoint[0], endPoint[1]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pointsOnCurve;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBezierCurveArcLengths = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
endPoint: Point,
|
|
||||||
) => {
|
|
||||||
const arcLengths: number[] = [];
|
|
||||||
arcLengths[0] = 0;
|
|
||||||
const points = getPointsInBezierCurve(element, endPoint);
|
|
||||||
let index = 0;
|
|
||||||
let distance = 0;
|
|
||||||
while (index < points.length - 1) {
|
|
||||||
const segmentDistance = distance2d(
|
|
||||||
points[index][0],
|
|
||||||
points[index][1],
|
|
||||||
points[index + 1][0],
|
|
||||||
points[index + 1][1],
|
|
||||||
);
|
|
||||||
distance += segmentDistance;
|
|
||||||
arcLengths.push(distance);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return arcLengths;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBezierCurveLength = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
endPoint: Point,
|
|
||||||
) => {
|
|
||||||
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
|
||||||
return arcLengths.at(-1) as number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length
|
|
||||||
export const mapIntervalToBezierT = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
endPoint: Point,
|
|
||||||
interval: number, // The interval between 0 to 1 for which you want to find the point on the curve,
|
|
||||||
) => {
|
|
||||||
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
|
||||||
const pointsCount = arcLengths.length - 1;
|
|
||||||
const curveLength = arcLengths.at(-1) as number;
|
|
||||||
const targetLength = interval * curveLength;
|
|
||||||
let low = 0;
|
|
||||||
let high = pointsCount;
|
|
||||||
let index = 0;
|
|
||||||
// Doing a binary search to find the largest length that is less than the target length
|
|
||||||
while (low < high) {
|
|
||||||
index = Math.floor(low + (high - low) / 2);
|
|
||||||
if (arcLengths[index] < targetLength) {
|
|
||||||
low = index + 1;
|
|
||||||
} else {
|
|
||||||
high = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (arcLengths[index] > targetLength) {
|
|
||||||
index--;
|
|
||||||
}
|
|
||||||
if (arcLengths[index] === targetLength) {
|
|
||||||
return index / pointsCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
1 -
|
|
||||||
(index +
|
|
||||||
(targetLength - arcLengths[index]) /
|
|
||||||
(arcLengths[index + 1] - arcLengths[index])) /
|
|
||||||
pointsCount
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const arePointsEqual = (p1: Point, p2: Point) => {
|
|
||||||
return p1[0] === p2[0] && p1[1] === p2[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isRightAngle = (angle: number) => {
|
|
||||||
// if our angles were mathematically accurate, we could just check
|
|
||||||
//
|
|
||||||
// angle % (Math.PI / 2) === 0
|
|
||||||
//
|
|
||||||
// but since we're in floating point land, we need to round.
|
|
||||||
//
|
|
||||||
// Below, after dividing by Math.PI, a multiple of 0.5 indicates a right
|
|
||||||
// angle, which we can check with modulo after rounding.
|
|
||||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const radianToDegree = (r: number) => {
|
|
||||||
return (r * 180) / Math.PI;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const degreeToRadian = (d: number) => {
|
|
||||||
return (d / 180) * Math.PI;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given two ranges, return if the two ranges overlap with each other
|
|
||||||
// e.g. [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5]
|
|
||||||
export const rangesOverlap = (
|
|
||||||
[a0, a1]: [number, number],
|
|
||||||
[b0, b1]: [number, number],
|
|
||||||
) => {
|
|
||||||
if (a0 <= b0) {
|
|
||||||
return a1 >= b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a0 >= b0) {
|
|
||||||
return b1 >= a0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given two ranges,return ther intersection of the two ranges if any
|
|
||||||
// e.g. the intersection of [1, 3] and [2, 4] is [2, 3]
|
|
||||||
export const rangeIntersection = (
|
|
||||||
rangeA: [number, number],
|
|
||||||
rangeB: [number, number],
|
|
||||||
): [number, number] | null => {
|
|
||||||
const rangeStart = Math.max(rangeA[0], rangeB[0]);
|
|
||||||
const rangeEnd = Math.min(rangeA[1], rangeB[1]);
|
|
||||||
|
|
||||||
if (rangeStart <= rangeEnd) {
|
|
||||||
return [rangeStart, rangeEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValueInRange = (value: number, min: number, max: number) => {
|
|
||||||
return value >= min && value <= max;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const translatePoint = (p: Point, v: Vector): Point => [
|
|
||||||
p[0] + v[0],
|
|
||||||
p[1] + v[1],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const scaleVector = (v: Vector, scalar: number): Vector => [
|
|
||||||
v[0] * scalar,
|
|
||||||
v[1] * scalar,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const pointToVector = (p: Point, origin: Point = [0, 0]): Vector => [
|
|
||||||
p[0] - origin[0],
|
|
||||||
p[1] - origin[1],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const scalePointFromOrigin = (
|
|
||||||
p: Point,
|
|
||||||
mid: Point,
|
|
||||||
multiplier: number,
|
|
||||||
) => translatePoint(mid, scaleVector(pointToVector(p, mid), multiplier));
|
|
||||||
|
|
||||||
const triangleSign = (p1: Point, p2: Point, p3: Point): number =>
|
|
||||||
(p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]);
|
|
||||||
|
|
||||||
export const PointInTriangle = (pt: Point, v1: Point, v2: Point, v3: Point) => {
|
|
||||||
const d1 = triangleSign(pt, v1, v2);
|
|
||||||
const d2 = triangleSign(pt, v2, v3);
|
|
||||||
const d3 = triangleSign(pt, v3, v1);
|
|
||||||
|
|
||||||
const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
|
|
||||||
const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
|
|
||||||
|
|
||||||
return !(has_neg && has_pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const magnitudeSq = (vector: Vector) =>
|
|
||||||
vector[0] * vector[0] + vector[1] * vector[1];
|
|
||||||
|
|
||||||
export const magnitude = (vector: Vector) => Math.sqrt(magnitudeSq(vector));
|
|
||||||
|
|
||||||
export const normalize = (vector: Vector): Vector => {
|
|
||||||
const m = magnitude(vector);
|
|
||||||
|
|
||||||
return [vector[0] / m, vector[1] / m];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addVectors = (
|
|
||||||
vec1: Readonly<Vector>,
|
|
||||||
vec2: Readonly<Vector>,
|
|
||||||
): Vector => [vec1[0] + vec2[0], vec1[1] + vec2[1]];
|
|
||||||
|
|
||||||
export const subtractVectors = (
|
|
||||||
vec1: Readonly<Vector>,
|
|
||||||
vec2: Readonly<Vector>,
|
|
||||||
): Vector => [vec1[0] - vec2[0], vec1[1] - vec2[1]];
|
|
||||||
|
|
||||||
export const pointInsideBounds = (p: Point, bounds: Bounds): boolean =>
|
|
||||||
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the axis-aligned bounding box for a given element
|
|
||||||
*/
|
|
||||||
export const aabbForElement = (
|
|
||||||
element: Readonly<ExcalidrawElement>,
|
|
||||||
offset?: [number, number, number, number],
|
|
||||||
) => {
|
|
||||||
const bbox = {
|
|
||||||
minX: element.x,
|
|
||||||
minY: element.y,
|
|
||||||
maxX: element.x + element.width,
|
|
||||||
maxY: element.y + element.height,
|
|
||||||
midX: element.x + element.width / 2,
|
|
||||||
midY: element.y + element.height / 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
const center = [bbox.midX, bbox.midY] as Point;
|
|
||||||
const [topLeftX, topLeftY] = rotatePoint(
|
|
||||||
[bbox.minX, bbox.minY],
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const [topRightX, topRightY] = rotatePoint(
|
|
||||||
[bbox.maxX, bbox.minY],
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const [bottomRightX, bottomRightY] = rotatePoint(
|
|
||||||
[bbox.maxX, bbox.maxY],
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const [bottomLeftX, bottomLeftY] = rotatePoint(
|
|
||||||
[bbox.minX, bbox.maxY],
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const bounds = [
|
|
||||||
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
|
||||||
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
|
||||||
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
|
||||||
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
|
||||||
] as Bounds;
|
|
||||||
|
|
||||||
if (offset) {
|
|
||||||
const [topOffset, rightOffset, downOffset, leftOffset] = offset;
|
|
||||||
return [
|
|
||||||
bounds[0] - leftOffset,
|
|
||||||
bounds[1] - topOffset,
|
|
||||||
bounds[2] + rightOffset,
|
|
||||||
bounds[3] + downOffset,
|
|
||||||
] as Bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bounds;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PolarCoords = [number, number];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the polar coordinates for the given carthesian point represented by
|
|
||||||
* (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 carthesian2Polar = ([x, y]: Point): PolarCoords => [
|
|
||||||
Math.hypot(x, y),
|
|
||||||
Math.atan2(y, x),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
|
|
||||||
* corresponds to (1, 0) carthesian coordinates (point), i.e. to the "right".
|
|
||||||
*/
|
|
||||||
type SymmetricArc = { radius: number; startAngle: number; endAngle: number };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a carthesian point lies on a symmetric arc, i.e. an arc which
|
|
||||||
* is part of a circle contour centered on 0, 0.
|
|
||||||
*/
|
|
||||||
export const isPointOnSymmetricArc = (
|
|
||||||
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
|
||||||
point: Point,
|
|
||||||
): boolean => {
|
|
||||||
const [radius, angle] = carthesian2Polar(point);
|
|
||||||
|
|
||||||
return startAngle < endAngle
|
|
||||||
? Math.abs(radius - arcRadius) < 0.0000001 &&
|
|
||||||
startAngle <= angle &&
|
|
||||||
endAngle >= angle
|
|
||||||
: startAngle <= angle || endAngle >= angle;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCenterForBounds = (bounds: Bounds): Point => [
|
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
|
||||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getCenterForElement = (element: ExcalidrawElement): Point => [
|
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
|
||||||
pointInsideBounds([a[0], a[1]], b) ||
|
|
||||||
pointInsideBounds([a[2], a[1]], b) ||
|
|
||||||
pointInsideBounds([a[2], a[3]], b) ||
|
|
||||||
pointInsideBounds([a[0], a[3]], b) ||
|
|
||||||
pointInsideBounds([b[0], b[1]], a) ||
|
|
||||||
pointInsideBounds([b[2], b[1]], a) ||
|
|
||||||
pointInsideBounds([b[2], b[3]], a) ||
|
|
||||||
pointInsideBounds([b[0], b[3]], a);
|
|
||||||
|
|
||||||
export const clamp = (value: number, min: number, max: number) => {
|
|
||||||
return Math.min(Math.max(value, min), max);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const round = (value: number, precision: number) => {
|
|
||||||
const multiplier = Math.pow(10, precision);
|
|
||||||
return Math.round((value + Number.EPSILON) * multiplier) / multiplier;
|
|
||||||
};
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Point } from "./types";
|
import { pointFromPair, type GlobalPoint, type LocalPoint } from "../math";
|
||||||
|
|
||||||
export const getSizeFromPoints = (points: readonly Point[]) => {
|
export const getSizeFromPoints = (
|
||||||
|
points: readonly (GlobalPoint | LocalPoint)[],
|
||||||
|
) => {
|
||||||
const xs = points.map((point) => point[0]);
|
const xs = points.map((point) => point[0]);
|
||||||
const ys = points.map((point) => point[1]);
|
const ys = points.map((point) => point[1]);
|
||||||
return {
|
return {
|
||||||
|
@ -10,7 +12,7 @@ export const getSizeFromPoints = (points: readonly Point[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @arg dimension, 0 for rescaling only x, 1 for y */
|
/** @arg dimension, 0 for rescaling only x, 1 for y */
|
||||||
export const rescalePoints = (
|
export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
|
||||||
dimension: 0 | 1,
|
dimension: 0 | 1,
|
||||||
newSize: number,
|
newSize: number,
|
||||||
points: readonly Point[],
|
points: readonly Point[],
|
||||||
|
@ -31,7 +33,7 @@ export const rescalePoints = (
|
||||||
if (newCoordinate < nextMinCoordinate) {
|
if (newCoordinate < nextMinCoordinate) {
|
||||||
nextMinCoordinate = newCoordinate;
|
nextMinCoordinate = newCoordinate;
|
||||||
}
|
}
|
||||||
return newPoint as unknown as Point;
|
return newPoint as Point;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!normalize) {
|
if (!normalize) {
|
||||||
|
@ -45,11 +47,13 @@ export const rescalePoints = (
|
||||||
|
|
||||||
const translation = minCoordinate - nextMinCoordinate;
|
const translation = minCoordinate - nextMinCoordinate;
|
||||||
|
|
||||||
const nextPoints = scaledPoints.map(
|
const nextPoints = scaledPoints.map((scaledPoint) =>
|
||||||
(scaledPoint) =>
|
pointFromPair<Point>(
|
||||||
scaledPoint.map((value, currentDimension) => {
|
scaledPoint.map((value, currentDimension) => {
|
||||||
return currentDimension === dimension ? value + translation : value;
|
return currentDimension === dimension ? value + translation : value;
|
||||||
}) as [number, number],
|
}) as [number, number],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return nextPoints;
|
return nextPoints;
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { arrayToMap, throttleRAF } from "../utils";
|
import { arrayToMap, throttleRAF } from "../utils";
|
||||||
import type { InteractiveCanvasAppState, Point } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
|
import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
|
||||||
|
|
||||||
import { renderSnaps } from "../renderer/renderSnaps";
|
import { renderSnaps } from "../renderer/renderSnaps";
|
||||||
|
@ -69,7 +69,8 @@ import type {
|
||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { getCornerRadius } from "../math";
|
import type { GlobalPoint, LocalPoint, Radians } from "../../math";
|
||||||
|
import { getCornerRadius } from "../shapes";
|
||||||
|
|
||||||
const renderLinearElementPointHighlight = (
|
const renderLinearElementPointHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
@ -101,7 +102,7 @@ const renderLinearElementPointHighlight = (
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const highlightPoint = (
|
const highlightPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||||
point: Point,
|
point: Point,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
|
@ -168,7 +169,7 @@ const strokeDiamondWithRotation = (
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSingleLinearPoint = (
|
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
point: Point,
|
point: Point,
|
||||||
|
@ -499,7 +500,7 @@ const renderLinearPointHandles = (
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
).filter((midPoint) => midPoint !== null) as Point[];
|
).filter((midPoint): midPoint is GlobalPoint => midPoint !== null);
|
||||||
|
|
||||||
midPoints.forEach((segmentMidPoint) => {
|
midPoints.forEach((segmentMidPoint) => {
|
||||||
if (
|
if (
|
||||||
|
@ -931,7 +932,7 @@ const _renderInteractiveScene = ({
|
||||||
context.setLineDash(initialLineDash);
|
context.setLineDash(initialLineDash);
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
||||||
0,
|
0 as Radians,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
"mouse",
|
"mouse",
|
||||||
isFrameSelected
|
isFrameSelected
|
||||||
|
|
|
@ -27,7 +27,6 @@ import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { distance, getFontString, isRTL } from "../utils";
|
import { distance, getFontString, isRTL } from "../utils";
|
||||||
import { getCornerRadius, isRightAngle } from "../math";
|
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -60,6 +59,8 @@ 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 { 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
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
|
@ -907,7 +908,8 @@ export const renderElement = (
|
||||||
(!element.angle ||
|
(!element.angle ||
|
||||||
// or check if angle is a right angle in which case we can still
|
// or check if angle is a right angle in which case we can still
|
||||||
// disable smoothing without adversely affecting the result
|
// disable smoothing without adversely affecting the result
|
||||||
isRightAngle(element.angle))
|
// We need less-than comparison because of FP artihmetic
|
||||||
|
isRightAngleRads(element.angle))
|
||||||
) {
|
) {
|
||||||
// Disabling smoothing makes output much sharper, especially for
|
// Disabling smoothing makes output much sharper, especially for
|
||||||
// text. Unless for non-right angles, where the aliasing is really
|
// text. Unless for non-right angles, where the aliasing is really
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
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";
|
||||||
import type { InteractiveCanvasAppState, Point } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
|
|
||||||
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
||||||
const SNAP_COLOR_DARK = "#ff0000";
|
const SNAP_COLOR_DARK = "#ff0000";
|
||||||
|
@ -85,7 +86,7 @@ const drawPointerSnapLine = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawCross = (
|
const drawCross = <Point extends LocalPoint | GlobalPoint>(
|
||||||
[x, y]: Point,
|
[x, y]: Point,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
@ -106,18 +107,18 @@ const drawCross = (
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawLine = (
|
const drawLine = <Point extends LocalPoint | GlobalPoint>(
|
||||||
from: Point,
|
from: Point,
|
||||||
to: Point,
|
to: Point,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
) => {
|
) => {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.lineTo(...from);
|
context.lineTo(from[0], from[1]);
|
||||||
context.lineTo(...to);
|
context.lineTo(to[0], to[1]);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawGapLine = (
|
const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
|
||||||
from: Point,
|
from: Point,
|
||||||
to: Point,
|
to: Point,
|
||||||
direction: "horizontal" | "vertical",
|
direction: "horizontal" | "vertical",
|
||||||
|
@ -138,24 +139,28 @@ const drawGapLine = (
|
||||||
const halfPoint = [(from[0] + to[0]) / 2, from[1]];
|
const halfPoint = [(from[0] + to[0]) / 2, from[1]];
|
||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine([from[0], from[1] - FULL], [from[0], from[1] + FULL], context);
|
drawLine(
|
||||||
|
point(from[0], from[1] - FULL),
|
||||||
|
point(from[0], from[1] + FULL),
|
||||||
|
context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
[halfPoint[0] - QUARTER, halfPoint[1] - HALF],
|
point(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
|
||||||
[halfPoint[0] - QUARTER, halfPoint[1] + HALF],
|
point(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
[halfPoint[0] + QUARTER, halfPoint[1] - HALF],
|
point(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
|
||||||
[halfPoint[0] + QUARTER, halfPoint[1] + HALF],
|
point(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine([to[0], to[1] - FULL], [to[0], to[1] + FULL], context);
|
drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context);
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
|
@ -164,24 +169,28 @@ const drawGapLine = (
|
||||||
const halfPoint = [from[0], (from[1] + to[1]) / 2];
|
const halfPoint = [from[0], (from[1] + to[1]) / 2];
|
||||||
// (1)
|
// (1)
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
drawLine([from[0] - FULL, from[1]], [from[0] + FULL, from[1]], context);
|
drawLine(
|
||||||
|
point(from[0] - FULL, from[1]),
|
||||||
|
point(from[0] + FULL, from[1]),
|
||||||
|
context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3)
|
// (3)
|
||||||
drawLine(
|
drawLine(
|
||||||
[halfPoint[0] - HALF, halfPoint[1] - QUARTER],
|
point(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
|
||||||
[halfPoint[0] + HALF, halfPoint[1] - QUARTER],
|
point(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
drawLine(
|
drawLine(
|
||||||
[halfPoint[0] - HALF, halfPoint[1] + QUARTER],
|
point(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
|
||||||
[halfPoint[0] + HALF, halfPoint[1] + QUARTER],
|
point(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appState.zenModeEnabled) {
|
if (!appState.zenModeEnabled) {
|
||||||
// (4)
|
// (4)
|
||||||
drawLine([to[0] - FULL, to[1]], [to[0] + FULL, to[1]], context);
|
drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context);
|
||||||
|
|
||||||
// (2)
|
// (2)
|
||||||
drawLine(from, to, context);
|
drawLine(from, to, context);
|
||||||
|
|
|
@ -30,13 +30,13 @@ import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getContainingFrame } from "../frame";
|
import { getContainingFrame } from "../frame";
|
||||||
import { getCornerRadius, isPathALoop } from "../math";
|
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
||||||
|
@ -9,7 +10,6 @@ import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { isPathALoop, getCornerRadius, distanceSq2d } from "../math";
|
|
||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
|
@ -23,6 +23,13 @@ import {
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import type { EmbedsValidationStatus } from "../types";
|
||||||
|
import {
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
} from "../../math";
|
||||||
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
|
@ -399,12 +406,14 @@ export const _generateElementShape = (
|
||||||
|
|
||||||
// points array can be empty in the beginning, so it is important to add
|
// points array can be empty in the beginning, so it is important to add
|
||||||
// initial position to it
|
// initial position to it
|
||||||
const points = element.points.length ? element.points : [[0, 0]];
|
const points = element.points.length
|
||||||
|
? element.points
|
||||||
|
: [point<LocalPoint>(0, 0)];
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points as [number, number][], 16),
|
generateElbowArrowShape(points, 16),
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -412,12 +421,16 @@ export const _generateElementShape = (
|
||||||
// curve is always the first element
|
// curve is always the first element
|
||||||
// this simplifies finding the curve for an element
|
// this simplifies finding the curve for an element
|
||||||
if (options.fill) {
|
if (options.fill) {
|
||||||
shape = [generator.polygon(points as [number, number][], options)];
|
shape = [
|
||||||
|
generator.polygon(points as unknown as RoughPoint[], options),
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
shape = [generator.linearPath(points as [number, number][], options)];
|
shape = [
|
||||||
|
generator.linearPath(points as unknown as RoughPoint[], options),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shape = [generator.curve(points as [number, number][], options)];
|
shape = [generator.curve(points as unknown as RoughPoint[], options)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// add lines only in arrow
|
// add lines only in arrow
|
||||||
|
@ -491,8 +504,8 @@ export const _generateElementShape = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateElbowArrowShape = (
|
const generateElbowArrowShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
points: [number, number][],
|
points: readonly Point[],
|
||||||
radius: number,
|
radius: number,
|
||||||
) => {
|
) => {
|
||||||
const subpoints = [] as [number, number][];
|
const subpoints = [] as [number, number][];
|
||||||
|
@ -501,8 +514,8 @@ const generateElbowArrowShape = (
|
||||||
const next = points[i + 1];
|
const next = points[i + 1];
|
||||||
const corner = Math.min(
|
const corner = Math.min(
|
||||||
radius,
|
radius,
|
||||||
Math.sqrt(distanceSq2d(points[i], next)) / 2,
|
pointDistance(points[i], next) / 2,
|
||||||
Math.sqrt(distanceSq2d(points[i], prev)) / 2,
|
pointDistance(points[i], prev) / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (prev[0] < points[i][0] && prev[1] === points[i][1]) {
|
if (prev[0] < points[i][0] && prev[1] === points[i][1]) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { clamp, round } from "../../math";
|
||||||
import { MAX_ZOOM, MIN_ZOOM } from "../constants";
|
import { MAX_ZOOM, MIN_ZOOM } from "../constants";
|
||||||
import { clamp, round } from "../math";
|
|
||||||
import type { NormalizedZoomValue } from "../types";
|
import type { NormalizedZoomValue } from "../types";
|
||||||
|
|
||||||
export const getNormalizedZoom = (zoom: number): NormalizedZoomValue => {
|
export const getNormalizedZoom = (zoom: number): NormalizedZoomValue => {
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
import {
|
||||||
|
isPoint,
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
pointFromPair,
|
||||||
|
pointRotateRads,
|
||||||
|
pointsEqual,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
} from "../math";
|
||||||
import {
|
import {
|
||||||
getClosedCurveShape,
|
getClosedCurveShape,
|
||||||
|
getCurvePathOps,
|
||||||
getCurveShape,
|
getCurveShape,
|
||||||
getEllipseShape,
|
getEllipseShape,
|
||||||
getFreedrawShape,
|
getFreedrawShape,
|
||||||
|
@ -18,13 +29,27 @@ import {
|
||||||
SelectionIcon,
|
SelectionIcon,
|
||||||
TextIcon,
|
TextIcon,
|
||||||
} from "./components/icons";
|
} from "./components/icons";
|
||||||
|
import {
|
||||||
|
DEFAULT_ADAPTIVE_RADIUS,
|
||||||
|
DEFAULT_PROPORTIONAL_RADIUS,
|
||||||
|
LINE_CONFIRM_THRESHOLD,
|
||||||
|
ROUNDNESS,
|
||||||
|
} from "./constants";
|
||||||
import { getElementAbsoluteCoords } from "./element";
|
import { getElementAbsoluteCoords } from "./element";
|
||||||
|
import type { Bounds } from "./element/bounds";
|
||||||
import { shouldTestInside } from "./element/collision";
|
import { shouldTestInside } from "./element/collision";
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "./element/types";
|
||||||
import { KEYS } from "./keys";
|
import { KEYS } from "./keys";
|
||||||
import { ShapeCache } from "./scene/ShapeCache";
|
import { ShapeCache } from "./scene/ShapeCache";
|
||||||
|
import type { NormalizedZoomValue, Zoom } from "./types";
|
||||||
|
import { invariant } from "./utils";
|
||||||
|
|
||||||
export const SHAPES = [
|
export const SHAPES = [
|
||||||
{
|
{
|
||||||
|
@ -116,10 +141,10 @@ export const findShapeByKey = (key: string) => {
|
||||||
* get the pure geometric shape of an excalidraw element
|
* get the pure geometric shape of an excalidraw element
|
||||||
* which is then used for hit detection
|
* which is then used for hit detection
|
||||||
*/
|
*/
|
||||||
export const getElementShape = (
|
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
@ -139,17 +164,19 @@ export const getElementShape = (
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
|
||||||
return shouldTestInside(element)
|
return shouldTestInside(element)
|
||||||
? getClosedCurveShape(
|
? getClosedCurveShape<Point>(
|
||||||
element,
|
element,
|
||||||
roughShape,
|
roughShape,
|
||||||
[element.x, element.y],
|
point<Point>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
[cx, cy],
|
point(cx, cy),
|
||||||
)
|
)
|
||||||
: getCurveShape(roughShape, [element.x, element.y], element.angle, [
|
: getCurveShape<Point>(
|
||||||
cx,
|
roughShape,
|
||||||
cy,
|
point<Point>(element.x, element.y),
|
||||||
]);
|
element.angle,
|
||||||
|
point(cx, cy),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
|
@ -157,15 +184,19 @@ export const getElementShape = (
|
||||||
|
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
return getFreedrawShape(element, [cx, cy], shouldTestInside(element));
|
return getFreedrawShape(
|
||||||
|
element,
|
||||||
|
point(cx, cy),
|
||||||
|
shouldTestInside(element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextShape = (
|
export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GeometricShape | null => {
|
): GeometricShape<Point> | null => {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
|
@ -189,3 +220,274 @@ export const getBoundTextShape = (
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getControlPointsForBezierCurve = <
|
||||||
|
P extends GlobalPoint | LocalPoint,
|
||||||
|
>(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
endPoint: P,
|
||||||
|
) => {
|
||||||
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
|
if (!shape) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
let currentP = point<P>(0, 0);
|
||||||
|
let index = 0;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
let controlPoints: P[] | null = null;
|
||||||
|
|
||||||
|
while (index < ops.length) {
|
||||||
|
const { op, data } = ops[index];
|
||||||
|
if (op === "move") {
|
||||||
|
invariant(
|
||||||
|
isPoint(data),
|
||||||
|
"The returned ops is not compatible with a point",
|
||||||
|
);
|
||||||
|
currentP = pointFromPair(data);
|
||||||
|
}
|
||||||
|
if (op === "bcurveTo") {
|
||||||
|
const p0 = currentP;
|
||||||
|
const p1 = point<P>(data[0], data[1]);
|
||||||
|
const p2 = point<P>(data[2], data[3]);
|
||||||
|
const p3 = point<P>(data[4], data[5]);
|
||||||
|
const distance = pointDistance(p3, endPoint);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
controlPoints = [p0, p1, p2, p3];
|
||||||
|
}
|
||||||
|
currentP = p3;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return controlPoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p0: P,
|
||||||
|
p1: P,
|
||||||
|
p2: P,
|
||||||
|
p3: P,
|
||||||
|
t: number,
|
||||||
|
): P => {
|
||||||
|
const equation = (t: number, idx: number) =>
|
||||||
|
Math.pow(1 - t, 3) * p3[idx] +
|
||||||
|
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
||||||
|
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
||||||
|
p0[idx] * Math.pow(t, 3);
|
||||||
|
const tx = equation(t, 0);
|
||||||
|
const ty = equation(t, 1);
|
||||||
|
return point(tx, ty);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
endPoint: P,
|
||||||
|
) => {
|
||||||
|
const controlPoints: P[] = getControlPointsForBezierCurve(element, endPoint)!;
|
||||||
|
if (!controlPoints) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const pointsOnCurve: P[] = [];
|
||||||
|
let t = 1;
|
||||||
|
// Take 20 points on curve for better accuracy
|
||||||
|
while (t > 0) {
|
||||||
|
const p = getBezierXY(
|
||||||
|
controlPoints[0],
|
||||||
|
controlPoints[1],
|
||||||
|
controlPoints[2],
|
||||||
|
controlPoints[3],
|
||||||
|
t,
|
||||||
|
);
|
||||||
|
pointsOnCurve.push(point(p[0], p[1]));
|
||||||
|
t -= 0.05;
|
||||||
|
}
|
||||||
|
if (pointsOnCurve.length) {
|
||||||
|
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
||||||
|
pointsOnCurve.push(point(endPoint[0], endPoint[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pointsOnCurve;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBezierCurveArcLengths = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
endPoint: P,
|
||||||
|
) => {
|
||||||
|
const arcLengths: number[] = [];
|
||||||
|
arcLengths[0] = 0;
|
||||||
|
const points = getPointsInBezierCurve(element, endPoint);
|
||||||
|
let index = 0;
|
||||||
|
let distance = 0;
|
||||||
|
while (index < points.length - 1) {
|
||||||
|
const segmentDistance = pointDistance(points[index], points[index + 1]);
|
||||||
|
distance += segmentDistance;
|
||||||
|
arcLengths.push(distance);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arcLengths;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBezierCurveLength = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
endPoint: P,
|
||||||
|
) => {
|
||||||
|
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
||||||
|
return arcLengths.at(-1) as number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length
|
||||||
|
export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
endPoint: P,
|
||||||
|
interval: number, // The interval between 0 to 1 for which you want to find the point on the curve,
|
||||||
|
) => {
|
||||||
|
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
||||||
|
const pointsCount = arcLengths.length - 1;
|
||||||
|
const curveLength = arcLengths.at(-1) as number;
|
||||||
|
const targetLength = interval * curveLength;
|
||||||
|
let low = 0;
|
||||||
|
let high = pointsCount;
|
||||||
|
let index = 0;
|
||||||
|
// Doing a binary search to find the largest length that is less than the target length
|
||||||
|
while (low < high) {
|
||||||
|
index = Math.floor(low + (high - low) / 2);
|
||||||
|
if (arcLengths[index] < targetLength) {
|
||||||
|
low = index + 1;
|
||||||
|
} else {
|
||||||
|
high = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arcLengths[index] > targetLength) {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
if (arcLengths[index] === targetLength) {
|
||||||
|
return index / pointsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
1 -
|
||||||
|
(index +
|
||||||
|
(targetLength - arcLengths[index]) /
|
||||||
|
(arcLengths[index + 1] - arcLengths[index])) /
|
||||||
|
pointsCount
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the axis-aligned bounding box for a given element
|
||||||
|
*/
|
||||||
|
export const aabbForElement = (
|
||||||
|
element: Readonly<ExcalidrawElement>,
|
||||||
|
offset?: [number, number, number, number],
|
||||||
|
) => {
|
||||||
|
const bbox = {
|
||||||
|
minX: element.x,
|
||||||
|
minY: element.y,
|
||||||
|
maxX: element.x + element.width,
|
||||||
|
maxY: element.y + element.height,
|
||||||
|
midX: element.x + element.width / 2,
|
||||||
|
midY: element.y + element.height / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const center = point(bbox.midX, bbox.midY);
|
||||||
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
|
point(bbox.minX, bbox.minY),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [topRightX, topRightY] = pointRotateRads(
|
||||||
|
point(bbox.maxX, bbox.minY),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [bottomRightX, bottomRightY] = pointRotateRads(
|
||||||
|
point(bbox.maxX, bbox.maxY),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
||||||
|
point(bbox.minX, bbox.maxY),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const bounds = [
|
||||||
|
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
||||||
|
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
||||||
|
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
||||||
|
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
||||||
|
] as Bounds;
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
const [topOffset, rightOffset, downOffset, leftOffset] = offset;
|
||||||
|
return [
|
||||||
|
bounds[0] - leftOffset,
|
||||||
|
bounds[1] - topOffset,
|
||||||
|
bounds[2] + rightOffset,
|
||||||
|
bounds[3] + downOffset,
|
||||||
|
] as Bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
bounds: Bounds,
|
||||||
|
): boolean =>
|
||||||
|
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
||||||
|
|
||||||
|
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
||||||
|
pointInsideBounds(point(a[0], a[1]), b) ||
|
||||||
|
pointInsideBounds(point(a[2], a[1]), b) ||
|
||||||
|
pointInsideBounds(point(a[2], a[3]), b) ||
|
||||||
|
pointInsideBounds(point(a[0], a[3]), b) ||
|
||||||
|
pointInsideBounds(point(b[0], b[1]), a) ||
|
||||||
|
pointInsideBounds(point(b[2], b[1]), a) ||
|
||||||
|
pointInsideBounds(point(b[2], b[3]), a) ||
|
||||||
|
pointInsideBounds(point(b[0], b[3]), a);
|
||||||
|
|
||||||
|
export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||||
|
if (
|
||||||
|
element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS ||
|
||||||
|
element.roundness?.type === ROUNDNESS.LEGACY
|
||||||
|
) {
|
||||||
|
return x * DEFAULT_PROPORTIONAL_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) {
|
||||||
|
const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS;
|
||||||
|
|
||||||
|
const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS;
|
||||||
|
|
||||||
|
if (x <= CUTOFF_SIZE) {
|
||||||
|
return x * DEFAULT_PROPORTIONAL_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedRadiusSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks if the first and last point are close enough
|
||||||
|
// to be considered a loop
|
||||||
|
export const isPathALoop = (
|
||||||
|
points: ExcalidrawLinearElement["points"],
|
||||||
|
/** supply if you want the loop detection to account for current zoom */
|
||||||
|
zoomValue: Zoom["value"] = 1 as NormalizedZoomValue,
|
||||||
|
): boolean => {
|
||||||
|
if (points.length >= 3) {
|
||||||
|
const [first, last] = [points[0], points[points.length - 1]];
|
||||||
|
const distance = pointDistance(first, last);
|
||||||
|
|
||||||
|
// Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in
|
||||||
|
// really close we make the threshold smaller, and vice versa.
|
||||||
|
return distance <= LINE_CONFIRM_THRESHOLD / zoomValue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
import type { InclusiveRange } from "../math";
|
||||||
|
import {
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
rangeInclusive,
|
||||||
|
rangeIntersection,
|
||||||
|
rangesOverlap,
|
||||||
|
type GlobalPoint,
|
||||||
|
} from "../math";
|
||||||
import { TOOL_TYPE } from "./constants";
|
import { TOOL_TYPE } from "./constants";
|
||||||
import type { Bounds } from "./element/bounds";
|
import type { Bounds } from "./element/bounds";
|
||||||
import {
|
import {
|
||||||
|
@ -14,7 +23,6 @@ import type {
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
import { KEYS } from "./keys";
|
import { KEYS } from "./keys";
|
||||||
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
|
|
||||||
import {
|
import {
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
getVisibleAndNonSelectedElements,
|
getVisibleAndNonSelectedElements,
|
||||||
|
@ -23,7 +31,7 @@ import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
KeyboardModifiersObject,
|
KeyboardModifiersObject,
|
||||||
Point,
|
NullableGridSize,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const SNAP_DISTANCE = 8;
|
const SNAP_DISTANCE = 8;
|
||||||
|
@ -42,7 +50,7 @@ type Vector2D = {
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PointPair = [Point, Point];
|
type PointPair = [GlobalPoint, GlobalPoint];
|
||||||
|
|
||||||
export type PointSnap = {
|
export type PointSnap = {
|
||||||
type: "point";
|
type: "point";
|
||||||
|
@ -62,9 +70,9 @@ export type Gap = {
|
||||||
// ↑ end side
|
// ↑ end side
|
||||||
startBounds: Bounds;
|
startBounds: Bounds;
|
||||||
endBounds: Bounds;
|
endBounds: Bounds;
|
||||||
startSide: [Point, Point];
|
startSide: [GlobalPoint, GlobalPoint];
|
||||||
endSide: [Point, Point];
|
endSide: [GlobalPoint, GlobalPoint];
|
||||||
overlap: [number, number];
|
overlap: InclusiveRange;
|
||||||
length: number;
|
length: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,7 +96,7 @@ export type Snaps = Snap[];
|
||||||
|
|
||||||
export type PointSnapLine = {
|
export type PointSnapLine = {
|
||||||
type: "points";
|
type: "points";
|
||||||
points: Point[];
|
points: GlobalPoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerSnapLine = {
|
export type PointerSnapLine = {
|
||||||
|
@ -108,14 +116,14 @@ export type SnapLine = PointSnapLine | GapSnapLine | PointerSnapLine;
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export class SnapCache {
|
export class SnapCache {
|
||||||
private static referenceSnapPoints: Point[] | null = null;
|
private static referenceSnapPoints: GlobalPoint[] | null = null;
|
||||||
|
|
||||||
private static visibleGaps: {
|
private static visibleGaps: {
|
||||||
verticalGaps: Gap[];
|
verticalGaps: Gap[];
|
||||||
horizontalGaps: Gap[];
|
horizontalGaps: Gap[];
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
public static setReferenceSnapPoints = (snapPoints: Point[] | null) => {
|
public static setReferenceSnapPoints = (snapPoints: GlobalPoint[] | null) => {
|
||||||
SnapCache.referenceSnapPoints = snapPoints;
|
SnapCache.referenceSnapPoints = snapPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -191,8 +199,8 @@ export const getElementsCorners = (
|
||||||
omitCenter: false,
|
omitCenter: false,
|
||||||
boundingBoxCorners: false,
|
boundingBoxCorners: false,
|
||||||
},
|
},
|
||||||
): Point[] => {
|
): GlobalPoint[] => {
|
||||||
let result: Point[] = [];
|
let result: GlobalPoint[] = [];
|
||||||
|
|
||||||
if (elements.length === 1) {
|
if (elements.length === 1) {
|
||||||
const element = elements[0];
|
const element = elements[0];
|
||||||
|
@ -219,33 +227,53 @@ export const getElementsCorners = (
|
||||||
(element.type === "diamond" || element.type === "ellipse") &&
|
(element.type === "diamond" || element.type === "ellipse") &&
|
||||||
!boundingBoxCorners
|
!boundingBoxCorners
|
||||||
) {
|
) {
|
||||||
const leftMid = rotatePoint(
|
const leftMid = pointRotateRads<GlobalPoint>(
|
||||||
[x1, y1 + halfHeight],
|
point(x1, y1 + halfHeight),
|
||||||
[cx, cy],
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const topMid = rotatePoint([x1 + halfWidth, y1], [cx, cy], element.angle);
|
const topMid = pointRotateRads<GlobalPoint>(
|
||||||
const rightMid = rotatePoint(
|
point(x1 + halfWidth, y1),
|
||||||
[x2, y1 + halfHeight],
|
point(cx, cy),
|
||||||
[cx, cy],
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const bottomMid = rotatePoint(
|
const rightMid = pointRotateRads<GlobalPoint>(
|
||||||
[x1 + halfWidth, y2],
|
point(x2, y1 + halfHeight),
|
||||||
[cx, cy],
|
point(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const center: Point = [cx, cy];
|
const bottomMid = pointRotateRads<GlobalPoint>(
|
||||||
|
point(x1 + halfWidth, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const center = point<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [leftMid, topMid, rightMid, bottomMid]
|
? [leftMid, topMid, rightMid, bottomMid]
|
||||||
: [leftMid, topMid, rightMid, bottomMid, center];
|
: [leftMid, topMid, rightMid, bottomMid, center];
|
||||||
} else {
|
} else {
|
||||||
const topLeft = rotatePoint([x1, y1], [cx, cy], element.angle);
|
const topLeft = pointRotateRads<GlobalPoint>(
|
||||||
const topRight = rotatePoint([x2, y1], [cx, cy], element.angle);
|
point(x1, y1),
|
||||||
const bottomLeft = rotatePoint([x1, y2], [cx, cy], element.angle);
|
point(cx, cy),
|
||||||
const bottomRight = rotatePoint([x2, y2], [cx, cy], element.angle);
|
element.angle,
|
||||||
const center: Point = [cx, cy];
|
);
|
||||||
|
const topRight = pointRotateRads<GlobalPoint>(
|
||||||
|
point(x2, y1),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const bottomLeft = pointRotateRads<GlobalPoint>(
|
||||||
|
point(x1, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const bottomRight = pointRotateRads<GlobalPoint>(
|
||||||
|
point(x2, y2),
|
||||||
|
point(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const center = point<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||||
|
@ -259,18 +287,18 @@ export const getElementsCorners = (
|
||||||
const width = maxX - minX;
|
const width = maxX - minX;
|
||||||
const height = maxY - minY;
|
const height = maxY - minY;
|
||||||
|
|
||||||
const topLeft: Point = [minX, minY];
|
const topLeft = point<GlobalPoint>(minX, minY);
|
||||||
const topRight: Point = [maxX, minY];
|
const topRight = point<GlobalPoint>(maxX, minY);
|
||||||
const bottomLeft: Point = [minX, maxY];
|
const bottomLeft = point<GlobalPoint>(minX, maxY);
|
||||||
const bottomRight: Point = [maxX, maxY];
|
const bottomRight = point<GlobalPoint>(maxX, maxY);
|
||||||
const center: Point = [minX + width / 2, minY + height / 2];
|
const center = point<GlobalPoint>(minX + width / 2, minY + height / 2);
|
||||||
|
|
||||||
result = omitCenter
|
result = omitCenter
|
||||||
? [topLeft, topRight, bottomLeft, bottomRight]
|
? [topLeft, topRight, bottomLeft, bottomRight]
|
||||||
: [topLeft, topRight, bottomLeft, bottomRight, center];
|
: [topLeft, topRight, bottomLeft, bottomRight, center];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map((point) => [round(point[0]), round(point[1])] as Point);
|
return result.map((p) => point(round(p[0]), round(p[1])));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReferenceElements = (
|
const getReferenceElements = (
|
||||||
|
@ -339,23 +367,20 @@ export const getVisibleGaps = (
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startMaxX < endMinX &&
|
startMaxX < endMinX &&
|
||||||
rangesOverlap([startMinY, startMaxY], [endMinY, endMaxY])
|
rangesOverlap(
|
||||||
|
rangeInclusive(startMinY, startMaxY),
|
||||||
|
rangeInclusive(endMinY, endMaxY),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
horizontalGaps.push({
|
horizontalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [
|
startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
|
||||||
[startMaxX, startMinY],
|
endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
|
||||||
[startMaxX, startMaxY],
|
|
||||||
],
|
|
||||||
endSide: [
|
|
||||||
[endMinX, endMinY],
|
|
||||||
[endMinX, endMaxY],
|
|
||||||
],
|
|
||||||
length: endMinX - startMaxX,
|
length: endMinX - startMaxX,
|
||||||
overlap: rangeIntersection(
|
overlap: rangeIntersection(
|
||||||
[startMinY, startMaxY],
|
rangeInclusive(startMinY, startMaxY),
|
||||||
[endMinY, endMaxY],
|
rangeInclusive(endMinY, endMaxY),
|
||||||
)!,
|
)!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -382,23 +407,20 @@ export const getVisibleGaps = (
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startMaxY < endMinY &&
|
startMaxY < endMinY &&
|
||||||
rangesOverlap([startMinX, startMaxX], [endMinX, endMaxX])
|
rangesOverlap(
|
||||||
|
rangeInclusive(startMinX, startMaxX),
|
||||||
|
rangeInclusive(endMinX, endMaxX),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
verticalGaps.push({
|
verticalGaps.push({
|
||||||
startBounds,
|
startBounds,
|
||||||
endBounds,
|
endBounds,
|
||||||
startSide: [
|
startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
|
||||||
[startMinX, startMaxY],
|
endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
|
||||||
[startMaxX, startMaxY],
|
|
||||||
],
|
|
||||||
endSide: [
|
|
||||||
[endMinX, endMinY],
|
|
||||||
[endMaxX, endMinY],
|
|
||||||
],
|
|
||||||
length: endMinY - startMaxY,
|
length: endMinY - startMaxY,
|
||||||
overlap: rangeIntersection(
|
overlap: rangeIntersection(
|
||||||
[startMinX, startMaxX],
|
rangeInclusive(startMinX, startMaxX),
|
||||||
[endMinX, endMaxX],
|
rangeInclusive(endMinX, endMaxX),
|
||||||
)!,
|
)!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -441,7 +463,7 @@ const getGapSnaps = (
|
||||||
const centerY = (minY + maxY) / 2;
|
const centerY = (minY + maxY) / 2;
|
||||||
|
|
||||||
for (const gap of horizontalGaps) {
|
for (const gap of horizontalGaps) {
|
||||||
if (!rangesOverlap([minY, maxY], gap.overlap)) {
|
if (!rangesOverlap(rangeInclusive(minY, maxY), gap.overlap)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,7 +532,7 @@ const getGapSnaps = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const gap of verticalGaps) {
|
for (const gap of verticalGaps) {
|
||||||
if (!rangesOverlap([minX, maxX], gap.overlap)) {
|
if (!rangesOverlap(rangeInclusive(minX, maxX), gap.overlap)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,7 +625,7 @@ export const getReferenceSnapPoints = (
|
||||||
|
|
||||||
const getPointSnaps = (
|
const getPointSnaps = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
selectionSnapPoints: Point[],
|
selectionSnapPoints: GlobalPoint[],
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
event: KeyboardModifiersObject,
|
event: KeyboardModifiersObject,
|
||||||
nearestSnapsX: Snaps,
|
nearestSnapsX: Snaps,
|
||||||
|
@ -779,8 +801,8 @@ const round = (x: number) => {
|
||||||
return Math.round(x * 10 ** decimalPlaces) / 10 ** decimalPlaces;
|
return Math.round(x * 10 ** decimalPlaces) / 10 ** decimalPlaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dedupePoints = (points: Point[]): Point[] => {
|
const dedupePoints = (points: GlobalPoint[]): GlobalPoint[] => {
|
||||||
const map = new Map<string, Point>();
|
const map = new Map<string, GlobalPoint>();
|
||||||
|
|
||||||
for (const point of points) {
|
for (const point of points) {
|
||||||
const key = point.join(",");
|
const key = point.join(",");
|
||||||
|
@ -797,8 +819,8 @@ const createPointSnapLines = (
|
||||||
nearestSnapsX: Snaps,
|
nearestSnapsX: Snaps,
|
||||||
nearestSnapsY: Snaps,
|
nearestSnapsY: Snaps,
|
||||||
): PointSnapLine[] => {
|
): PointSnapLine[] => {
|
||||||
const snapsX = {} as { [key: string]: Point[] };
|
const snapsX = {} as { [key: string]: GlobalPoint[] };
|
||||||
const snapsY = {} as { [key: string]: Point[] };
|
const snapsY = {} as { [key: string]: GlobalPoint[] };
|
||||||
|
|
||||||
if (nearestSnapsX.length > 0) {
|
if (nearestSnapsX.length > 0) {
|
||||||
for (const snap of nearestSnapsX) {
|
for (const snap of nearestSnapsX) {
|
||||||
|
@ -809,8 +831,8 @@ const createPointSnapLines = (
|
||||||
snapsX[key] = [];
|
snapsX[key] = [];
|
||||||
}
|
}
|
||||||
snapsX[key].push(
|
snapsX[key].push(
|
||||||
...snap.points.map(
|
...snap.points.map((p) =>
|
||||||
(point) => [round(point[0]), round(point[1])] as Point,
|
point<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -826,8 +848,8 @@ const createPointSnapLines = (
|
||||||
snapsY[key] = [];
|
snapsY[key] = [];
|
||||||
}
|
}
|
||||||
snapsY[key].push(
|
snapsY[key].push(
|
||||||
...snap.points.map(
|
...snap.points.map((p) =>
|
||||||
(point) => [round(point[0]), round(point[1])] as Point,
|
point<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -840,8 +862,8 @@ const createPointSnapLines = (
|
||||||
type: "points",
|
type: "points",
|
||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((point) => {
|
.map((p) => {
|
||||||
return [Number(key), point[1]] as Point;
|
return point<GlobalPoint>(Number(key), p[1]);
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[1] - b[1]),
|
.sort((a, b) => a[1] - b[1]),
|
||||||
),
|
),
|
||||||
|
@ -853,8 +875,8 @@ const createPointSnapLines = (
|
||||||
type: "points",
|
type: "points",
|
||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
points
|
||||||
.map((point) => {
|
.map((p) => {
|
||||||
return [point[0], Number(key)] as Point;
|
return point<GlobalPoint>(p[0], Number(key));
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[0] - b[0]),
|
.sort((a, b) => a[0] - b[0]),
|
||||||
),
|
),
|
||||||
|
@ -898,12 +920,12 @@ const createGapSnapLines = (
|
||||||
const [endMinX, endMinY, endMaxX, endMaxY] = gapSnap.gap.endBounds;
|
const [endMinX, endMinY, endMaxX, endMaxY] = gapSnap.gap.endBounds;
|
||||||
|
|
||||||
const verticalIntersection = rangeIntersection(
|
const verticalIntersection = rangeIntersection(
|
||||||
[minY, maxY],
|
rangeInclusive(minY, maxY),
|
||||||
gapSnap.gap.overlap,
|
gapSnap.gap.overlap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const horizontalGapIntersection = rangeIntersection(
|
const horizontalGapIntersection = rangeIntersection(
|
||||||
[minX, maxX],
|
rangeInclusive(minX, maxX),
|
||||||
gapSnap.gap.overlap,
|
gapSnap.gap.overlap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -918,16 +940,16 @@ const createGapSnapLines = (
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
[gapSnap.gap.startSide[0][0], gapLineY],
|
point(gapSnap.gap.startSide[0][0], gapLineY),
|
||||||
[minX, gapLineY],
|
point(minX, gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [
|
||||||
[maxX, gapLineY],
|
point(maxX, gapLineY),
|
||||||
[gapSnap.gap.endSide[0][0], gapLineY],
|
point(gapSnap.gap.endSide[0][0], gapLineY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -944,16 +966,16 @@ const createGapSnapLines = (
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
[gapLineX, gapSnap.gap.startSide[0][1]],
|
point(gapLineX, gapSnap.gap.startSide[0][1]),
|
||||||
[gapLineX, minY],
|
point(gapLineX, minY),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [
|
||||||
[gapLineX, maxY],
|
point(gapLineX, maxY),
|
||||||
[gapLineX, gapSnap.gap.endSide[0][1]],
|
point(gapLineX, gapSnap.gap.endSide[0][1]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -969,18 +991,12 @@ const createGapSnapLines = (
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||||
[startMaxX, gapLineY],
|
|
||||||
[endMinX, gapLineY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
|
||||||
[endMaxX, gapLineY],
|
|
||||||
[minX, gapLineY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -995,18 +1011,12 @@ const createGapSnapLines = (
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
|
||||||
[maxX, gapLineY],
|
|
||||||
[startMinX, gapLineY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
points: [
|
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
|
||||||
[startMaxX, gapLineY],
|
|
||||||
[endMinX, gapLineY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1021,18 +1031,12 @@ const createGapSnapLines = (
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
|
||||||
[gapLineX, maxY],
|
|
||||||
[gapLineX, startMinY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||||
[gapLineX, startMaxY],
|
|
||||||
[gapLineX, endMinY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1047,18 +1051,12 @@ const createGapSnapLines = (
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
|
||||||
[gapLineX, startMaxY],
|
|
||||||
[gapLineX, endMinY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "gap",
|
type: "gap",
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
points: [
|
points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
|
||||||
[gapLineX, endMaxY],
|
|
||||||
[gapLineX, minY],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1071,8 +1069,8 @@ const createGapSnapLines = (
|
||||||
gapSnapLines.map((gapSnapLine) => {
|
gapSnapLines.map((gapSnapLine) => {
|
||||||
return {
|
return {
|
||||||
...gapSnapLine,
|
...gapSnapLine,
|
||||||
points: gapSnapLine.points.map(
|
points: gapSnapLine.points.map((p) =>
|
||||||
(point) => [round(point[0]), round(point[1])] as Point,
|
point(round(p[0]), round(p[1])),
|
||||||
) as PointPair,
|
) as PointPair,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -1117,40 +1115,40 @@ export const snapResizingElements = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: Point[] = [];
|
const selectionSnapPoints: GlobalPoint[] = [];
|
||||||
|
|
||||||
if (transformHandle) {
|
if (transformHandle) {
|
||||||
switch (transformHandle) {
|
switch (transformHandle) {
|
||||||
case "e": {
|
case "e": {
|
||||||
selectionSnapPoints.push([maxX, minY], [maxX, maxY]);
|
selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "w": {
|
case "w": {
|
||||||
selectionSnapPoints.push([minX, minY], [minX, maxY]);
|
selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "n": {
|
case "n": {
|
||||||
selectionSnapPoints.push([minX, minY], [maxX, minY]);
|
selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "s": {
|
case "s": {
|
||||||
selectionSnapPoints.push([minX, maxY], [maxX, maxY]);
|
selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ne": {
|
case "ne": {
|
||||||
selectionSnapPoints.push([maxX, minY]);
|
selectionSnapPoints.push(point(maxX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "nw": {
|
case "nw": {
|
||||||
selectionSnapPoints.push([minX, minY]);
|
selectionSnapPoints.push(point(minX, minY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "se": {
|
case "se": {
|
||||||
selectionSnapPoints.push([maxX, maxY]);
|
selectionSnapPoints.push(point(maxX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sw": {
|
case "sw": {
|
||||||
selectionSnapPoints.push([minX, maxY]);
|
selectionSnapPoints.push(point(minX, maxY));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1192,11 +1190,11 @@ export const snapResizingElements = (
|
||||||
round(bound),
|
round(bound),
|
||||||
);
|
);
|
||||||
|
|
||||||
const corners: Point[] = [
|
const corners: GlobalPoint[] = [
|
||||||
[x1, y1],
|
point(x1, y1),
|
||||||
[x1, y2],
|
point(x1, y2),
|
||||||
[x2, y1],
|
point(x2, y1),
|
||||||
[x2, y2],
|
point(x2, y2),
|
||||||
];
|
];
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
|
@ -1232,8 +1230,8 @@ export const snapNewElement = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: Point[] = [
|
const selectionSnapPoints: GlobalPoint[] = [
|
||||||
[origin.x + dragOffset.x, origin.y + dragOffset.y],
|
point(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
||||||
];
|
];
|
||||||
|
|
||||||
const snapDistance = getSnapDistance(app.state.zoom.value);
|
const snapDistance = getSnapDistance(app.state.zoom.value);
|
||||||
|
@ -1333,7 +1331,7 @@ export const getSnapLinesAtPointer = (
|
||||||
|
|
||||||
verticalSnapLines.push({
|
verticalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, [corner[0], pointer.y]],
|
points: [corner, point(corner[0], pointer.y)],
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1349,7 +1347,7 @@ export const getSnapLinesAtPointer = (
|
||||||
|
|
||||||
horizontalSnapLines.push({
|
horizontalSnapLines.push({
|
||||||
type: "pointer",
|
type: "pointer",
|
||||||
points: [corner, [pointer.x, corner[1]]],
|
points: [corner, point(pointer.x, corner[1])],
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1386,3 +1384,18 @@ export const isActiveToolNonLinearSnappable = (
|
||||||
activeToolType === TOOL_TYPE.text
|
activeToolType === TOOL_TYPE.text
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Rounding this point causes some shake when free drawing
|
||||||
|
export const getGridPoint = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
gridSize: NullableGridSize,
|
||||||
|
): [number, number] => {
|
||||||
|
if (gridSize) {
|
||||||
|
return [
|
||||||
|
Math.round(x / gridSize) * gridSize,
|
||||||
|
Math.round(y / gridSize) * gridSize,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { API } from "./helpers/api";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -31,12 +32,7 @@ describe("element binding", () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 1,
|
height: 1,
|
||||||
points: [
|
points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)],
|
||||||
[0, 0],
|
|
||||||
[0, 0],
|
|
||||||
[100, 0],
|
|
||||||
[100, 0],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
API.setElements([rect, arrow]);
|
API.setElements([rect, arrow]);
|
||||||
expect(arrow.startBinding).toBe(null);
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
@ -314,10 +310,7 @@ describe("element binding", () => {
|
||||||
const arrow1 = API.createElement({
|
const arrow1 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
points: [
|
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||||
[0, 0],
|
|
||||||
[0, -87.45777932247563],
|
|
||||||
],
|
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
|
@ -335,10 +328,7 @@ describe("element binding", () => {
|
||||||
const arrow2 = API.createElement({
|
const arrow2 = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow2",
|
id: "arrow2",
|
||||||
points: [
|
points: [point(0, 0), point(0, -87.45777932247563)],
|
||||||
[0, 0],
|
|
||||||
[0, -87.45777932247563],
|
|
||||||
],
|
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "text1",
|
elementId: "text1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { Radians } from "../../../math";
|
||||||
import { DEFAULT_FONT_FAMILY } from "../../constants";
|
import { DEFAULT_FONT_FAMILY } from "../../constants";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
|
||||||
y: 237,
|
y: 237,
|
||||||
width: 214,
|
width: 214,
|
||||||
height: 214,
|
height: 214,
|
||||||
angle: 0,
|
angle: 0 as Radians,
|
||||||
strokeColor: "#000000",
|
strokeColor: "#000000",
|
||||||
backgroundColor: "#15aabf",
|
backgroundColor: "#15aabf",
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
|
|
|
@ -28,6 +28,8 @@ import { KEYS } from "../keys";
|
||||||
import { getBoundTextElementPosition } from "../element/textElement";
|
import { getBoundTextElementPosition } from "../element/textElement";
|
||||||
import { createPasteEvent } from "../clipboard";
|
import { createPasteEvent } from "../clipboard";
|
||||||
import { arrayToMap, cloneJSON } from "../utils";
|
import { arrayToMap, cloneJSON } from "../utils";
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
|
import { point, type Radians } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
@ -131,7 +133,7 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
|
||||||
y: -2412.5069664197654,
|
y: -2412.5069664197654,
|
||||||
width: 1750.4888916015625,
|
width: 1750.4888916015625,
|
||||||
height: 410.51605224609375,
|
height: 410.51605224609375,
|
||||||
angle: 0,
|
angle: 0 as Radians,
|
||||||
strokeColor: "#000000",
|
strokeColor: "#000000",
|
||||||
backgroundColor: "#fa5252",
|
backgroundColor: "#fa5252",
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
|
@ -145,9 +147,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
|
||||||
link: null,
|
link: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point<LocalPoint>(0, 0),
|
||||||
[-922.4761962890625, 300.3277587890625],
|
point<LocalPoint>(-922.4761962890625, 300.3277587890625),
|
||||||
[828.0126953125, 410.51605224609375],
|
point<LocalPoint>(828.0126953125, 410.51605224609375),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -423,8 +425,8 @@ describe("arrow", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => {
|
it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
API.setAppState({
|
API.setAppState({
|
||||||
|
@ -444,8 +446,8 @@ describe("arrow", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
|
it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
API.setAppState({
|
API.setAppState({
|
||||||
|
@ -477,8 +479,8 @@ describe("arrow", () => {
|
||||||
|
|
||||||
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
|
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
|
||||||
it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => {
|
it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
||||||
API.updateElement(line, { angle: originalAngle });
|
API.updateElement(line, { angle: originalAngle });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
|
@ -501,8 +503,8 @@ describe("arrow", () => {
|
||||||
|
|
||||||
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
|
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
|
||||||
it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => {
|
it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
||||||
API.updateElement(line, { angle: originalAngle });
|
API.updateElement(line, { angle: originalAngle });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
|
@ -585,8 +587,8 @@ describe("line", () => {
|
||||||
|
|
||||||
//TODO: elements with curve outside minMax points have a wrong bounding box
|
//TODO: elements with curve outside minMax points have a wrong bounding box
|
||||||
it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => {
|
it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
||||||
API.updateElement(line, { angle: originalAngle });
|
API.updateElement(line, { angle: originalAngle });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
|
@ -600,8 +602,8 @@ describe("line", () => {
|
||||||
|
|
||||||
//TODO: elements with curve outside minMax points have a wrong bounding box
|
//TODO: elements with curve outside minMax points have a wrong bounding box
|
||||||
it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => {
|
it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
||||||
API.updateElement(line, { angle: originalAngle });
|
API.updateElement(line, { angle: originalAngle });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
|
@ -619,8 +621,8 @@ describe("line", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips a rotated line horizontally with line inside min/max points bounds", async () => {
|
it("flips a rotated line horizontally with line inside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
API.setAppState({
|
API.setAppState({
|
||||||
|
@ -640,8 +642,8 @@ describe("line", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips a rotated line vertically with line inside min/max points bounds", async () => {
|
it("flips a rotated line vertically with line inside min/max points bounds", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
API.setAppState({
|
API.setAppState({
|
||||||
|
@ -772,8 +774,8 @@ describe("image", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips an rotated image horizontally correctly", async () => {
|
it("flips an rotated image horizontally correctly", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
//paste image
|
//paste image
|
||||||
await createImage();
|
await createImage();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
@ -790,8 +792,8 @@ describe("image", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips an rotated image vertically correctly", async () => {
|
it("flips an rotated image vertically correctly", async () => {
|
||||||
const originalAngle = Math.PI / 4;
|
const originalAngle = (Math.PI / 4) as Radians;
|
||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = ((7 * Math.PI) / 4) as Radians;
|
||||||
//paste image
|
//paste image
|
||||||
await createImage();
|
await createImage();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
newImageElement,
|
newImageElement,
|
||||||
newMagicFrameElement,
|
newMagicFrameElement,
|
||||||
} from "../../element/newElement";
|
} from "../../element/newElement";
|
||||||
import type { AppState, Point } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { getSelectedElements } from "../../scene/selection";
|
import { getSelectedElements } from "../../scene/selection";
|
||||||
import { isLinearElementType } from "../../element/typeChecks";
|
import { isLinearElementType } from "../../element/typeChecks";
|
||||||
import type { Mutable } from "../../utility-types";
|
import type { Mutable } from "../../utility-types";
|
||||||
|
@ -36,6 +36,7 @@ import type App from "../../components/App";
|
||||||
import { createTestHook } from "../../components/App";
|
import { createTestHook } from "../../components/App";
|
||||||
import type { Action } from "../../actions/types";
|
import type { Action } from "../../actions/types";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
import { point, type LocalPoint, type Radians } from "../../../math";
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
// so that window.h is available when App.tsx is not imported as well.
|
// so that window.h is available when App.tsx is not imported as well.
|
||||||
|
@ -171,7 +172,7 @@ export class API {
|
||||||
containerId?: T extends "text"
|
containerId?: T extends "text"
|
||||||
? ExcalidrawTextElement["containerId"]
|
? ExcalidrawTextElement["containerId"]
|
||||||
: never;
|
: never;
|
||||||
points?: T extends "arrow" | "line" ? readonly Point[] : never;
|
points?: T extends "arrow" | "line" ? readonly LocalPoint[] : never;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
fileId?: T extends "image" ? string : never;
|
fileId?: T extends "image" ? string : never;
|
||||||
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
||||||
|
@ -218,7 +219,7 @@ export class API {
|
||||||
y,
|
y,
|
||||||
frameId: rest.frameId ?? null,
|
frameId: rest.frameId ?? null,
|
||||||
index: rest.index ?? null,
|
index: rest.index ?? null,
|
||||||
angle: rest.angle ?? 0,
|
angle: (rest.angle ?? 0) as Radians,
|
||||||
strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
|
strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
rest.backgroundColor ?? appState.currentItemBackgroundColor,
|
rest.backgroundColor ?? appState.currentItemBackgroundColor,
|
||||||
|
@ -293,8 +294,8 @@ export class API {
|
||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
[0, 0],
|
point<LocalPoint>(0, 0),
|
||||||
[100, 100],
|
point<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
elbowed: rest.elbowed ?? false,
|
elbowed: rest.elbowed ?? false,
|
||||||
});
|
});
|
||||||
|
@ -306,8 +307,8 @@ export class API {
|
||||||
height,
|
height,
|
||||||
type,
|
type,
|
||||||
points: rest.points ?? [
|
points: rest.points ?? [
|
||||||
[0, 0],
|
point<LocalPoint>(0, 0),
|
||||||
[100, 100],
|
point<LocalPoint>(100, 100),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Point, ToolType } from "../../types";
|
import type { ToolType } from "../../types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -30,10 +30,11 @@ import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
} from "../../element/typeChecks";
|
} from "../../element/typeChecks";
|
||||||
import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
|
import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
|
||||||
import { rotatePoint } from "../../math";
|
|
||||||
import { getTextEditor } from "../queries/dom";
|
import { getTextEditor } from "../queries/dom";
|
||||||
import { arrayToMap } from "../../utils";
|
import { arrayToMap } from "../../utils";
|
||||||
import { createTestHook } from "../../components/App";
|
import { createTestHook } from "../../components/App";
|
||||||
|
import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
|
||||||
|
import { point, pointRotateRads } from "../../../math";
|
||||||
|
|
||||||
// so that window.h is available when App.tsx is not imported as well.
|
// so that window.h is available when App.tsx is not imported as well.
|
||||||
createTestHook();
|
createTestHook();
|
||||||
|
@ -131,27 +132,29 @@ export class Keyboard {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getElementPointForSelection = (element: ExcalidrawElement): Point => {
|
const getElementPointForSelection = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const target: Point = [
|
const target = point<GlobalPoint>(
|
||||||
x +
|
x +
|
||||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||||
y,
|
y,
|
||||||
];
|
);
|
||||||
let center: Point;
|
let center: GlobalPoint;
|
||||||
|
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element)) {
|
||||||
const bounds = getElementPointsCoords(element, element.points);
|
const bounds = getElementPointsCoords(element, element.points);
|
||||||
center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2];
|
center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2);
|
||||||
} else {
|
} else {
|
||||||
center = [x + width / 2, y + height / 2];
|
center = point(x + width / 2, y + height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
return center;
|
return center;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rotatePoint(target, center, angle);
|
return pointRotateRads(target, center, angle);
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Pointer {
|
export class Pointer {
|
||||||
|
@ -328,7 +331,7 @@ const transform = (
|
||||||
const isFrameSelected = elements.some(isFrameLikeElement);
|
const isFrameSelected = elements.some(isFrameLikeElement);
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
||||||
0,
|
0 as Radians,
|
||||||
h.state.zoom,
|
h.state.zoom,
|
||||||
"mouse",
|
"mouse",
|
||||||
isFrameSelected ? OMIT_SIDES_FOR_FRAME : OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
isFrameSelected ? OMIT_SIDES_FOR_FRAME : OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
|
@ -450,7 +453,7 @@ export class UI {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
points?: T extends "line" | "arrow" | "freedraw" ? Point[] : never;
|
points?: T extends "line" | "arrow" | "freedraw" ? LocalPoint[] : never;
|
||||||
} = {},
|
} = {},
|
||||||
): Element<T> & {
|
): Element<T> & {
|
||||||
/** Returns the actual, current element from the elements array, instead
|
/** Returns the actual, current element from the elements array, instead
|
||||||
|
@ -459,9 +462,9 @@ export class UI {
|
||||||
} {
|
} {
|
||||||
const width = initialWidth ?? initialHeight ?? size;
|
const width = initialWidth ?? initialHeight ?? size;
|
||||||
const height = initialHeight ?? size;
|
const height = initialHeight ?? size;
|
||||||
const points: Point[] = initialPoints ?? [
|
const points: LocalPoint[] = initialPoints ?? [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[width, height],
|
point(width, height),
|
||||||
];
|
];
|
||||||
|
|
||||||
UI.clickTool(type);
|
UI.clickTool(type);
|
||||||
|
|
|
@ -44,6 +44,8 @@ import { queryByText } from "@testing-library/react";
|
||||||
import { HistoryEntry } from "../history";
|
import { HistoryEntry } from "../history";
|
||||||
import { AppStateChange, ElementsChange } from "../change";
|
import { AppStateChange, ElementsChange } from "../change";
|
||||||
import { Snapshot, StoreAction } from "../store";
|
import { Snapshot, StoreAction } from "../store";
|
||||||
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -2038,9 +2040,9 @@ describe("history", () => {
|
||||||
width: 178.9000000000001,
|
width: 178.9000000000001,
|
||||||
height: 236.10000000000002,
|
height: 236.10000000000002,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[178.9000000000001, 0],
|
point(178.9000000000001, 0),
|
||||||
[178.9000000000001, 236.10000000000002],
|
point(178.9000000000001, 236.10000000000002),
|
||||||
],
|
],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
||||||
|
@ -2156,12 +2158,12 @@ describe("history", () => {
|
||||||
elements: [
|
elements: [
|
||||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[5, 5],
|
point(5, 5),
|
||||||
[10, 10],
|
point(10, 10),
|
||||||
[15, 15],
|
point(15, 15),
|
||||||
[20, 20],
|
point(20, 20),
|
||||||
],
|
] as LocalPoint[],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
|
@ -4003,7 +4005,7 @@ describe("history", () => {
|
||||||
newElementWith(h.elements[0], {
|
newElementWith(h.elements[0], {
|
||||||
x: 200,
|
x: 200,
|
||||||
y: 200,
|
y: 200,
|
||||||
angle: 90,
|
angle: 90 as Radians,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
@ -4121,7 +4123,7 @@ describe("history", () => {
|
||||||
newElementWith(h.elements[0], {
|
newElementWith(h.elements[0], {
|
||||||
x: 205,
|
x: 205,
|
||||||
y: 205,
|
y: 205,
|
||||||
angle: 90,
|
angle: 90 as Radians,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
|
|
@ -8,7 +8,6 @@ import type {
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw, mutateElement } from "../index";
|
||||||
import { centerPoint } from "../math";
|
|
||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
import * as StaticScene from "../renderer/staticScene";
|
import * as StaticScene from "../renderer/staticScene";
|
||||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||||
|
@ -16,7 +15,6 @@ import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||||
import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
|
import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import type { Point } from "../types";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
||||||
|
@ -29,6 +27,8 @@ import * as textElementUtils from "../element/textElement";
|
||||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import type { GlobalPoint } from "../../math";
|
||||||
|
import { pointCenter, point } from "../../math";
|
||||||
|
|
||||||
const renderInteractiveScene = vi.spyOn(
|
const renderInteractiveScene = vi.spyOn(
|
||||||
InteractiveCanvas,
|
InteractiveCanvas,
|
||||||
|
@ -57,9 +57,9 @@ describe("Test Linear Elements", () => {
|
||||||
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||||
});
|
});
|
||||||
|
|
||||||
const p1: Point = [20, 20];
|
const p1 = point<GlobalPoint>(20, 20);
|
||||||
const p2: Point = [60, 20];
|
const p2 = point<GlobalPoint>(60, 20);
|
||||||
const midpoint = centerPoint(p1, p2);
|
const midpoint = pointCenter<GlobalPoint>(p1, p2);
|
||||||
const delta = 50;
|
const delta = 50;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
@ -75,10 +75,7 @@ describe("Test Linear Elements", () => {
|
||||||
height: 0,
|
height: 0,
|
||||||
type,
|
type,
|
||||||
roughness,
|
roughness,
|
||||||
points: [
|
points: [point(0, 0), point(p2[0] - p1[0], p2[1] - p1[1])],
|
||||||
[0, 0],
|
|
||||||
[p2[0] - p1[0], p2[1] - p1[1]],
|
|
||||||
],
|
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
|
@ -102,9 +99,9 @@ describe("Test Linear Elements", () => {
|
||||||
type,
|
type,
|
||||||
roughness,
|
roughness,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[p3[0], p3[1]],
|
point(p3[0], p3[1]),
|
||||||
[p2[0] - p1[0], p2[1] - p1[1]],
|
point(p2[0] - p1[0], p2[1] - p1[1]),
|
||||||
],
|
],
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
|
@ -129,7 +126,7 @@ describe("Test Linear Elements", () => {
|
||||||
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const drag = (startPoint: Point, endPoint: Point) => {
|
const drag = (startPoint: GlobalPoint, endPoint: GlobalPoint) => {
|
||||||
fireEvent.pointerDown(interactiveCanvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: startPoint[0],
|
clientX: startPoint[0],
|
||||||
clientY: startPoint[1],
|
clientY: startPoint[1],
|
||||||
|
@ -144,7 +141,7 @@ describe("Test Linear Elements", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePoint = (point: Point) => {
|
const deletePoint = (point: GlobalPoint) => {
|
||||||
fireEvent.pointerDown(interactiveCanvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
clientY: point[1],
|
clientY: point[1],
|
||||||
|
@ -164,7 +161,7 @@ describe("Test Linear Elements", () => {
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
|
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
|
||||||
|
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
|
@ -172,7 +169,7 @@ describe("Test Linear Elements", () => {
|
||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(line.x).toBe(originalX);
|
expect(line.x).toBe(originalX);
|
||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
|
@ -187,7 +184,7 @@ describe("Test Linear Elements", () => {
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
|
@ -251,7 +248,7 @@ describe("Test Linear Elements", () => {
|
||||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
|
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1));
|
||||||
expect(line.x).toBe(originalX);
|
expect(line.x).toBe(originalX);
|
||||||
expect(line.y).toBe(originalY);
|
expect(line.y).toBe(originalY);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
|
@ -264,7 +261,7 @@ describe("Test Linear Elements", () => {
|
||||||
enterLineEditingMode(line);
|
enterLineEditingMode(line);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
|
@ -356,10 +353,13 @@ describe("Test Linear Elements", () => {
|
||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const startPoint = centerPoint(points[0], midPoints[0] as Point);
|
const startPoint = pointCenter(points[0], midPoints[0]!);
|
||||||
const deltaX = 50;
|
const deltaX = 50;
|
||||||
const deltaY = 20;
|
const deltaY = 20;
|
||||||
const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
|
const endPoint = point<GlobalPoint>(
|
||||||
|
startPoint[0] + deltaX,
|
||||||
|
startPoint[1] + deltaY,
|
||||||
|
);
|
||||||
|
|
||||||
// Move the element
|
// Move the element
|
||||||
drag(startPoint, endPoint);
|
drag(startPoint, endPoint);
|
||||||
|
@ -399,8 +399,8 @@ describe("Test Linear Elements", () => {
|
||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint: Point = [55, 45];
|
const firstSegmentMidpoint = point<GlobalPoint>(55, 45);
|
||||||
const lastSegmentMidpoint: Point = [75, 40];
|
const lastSegmentMidpoint = point<GlobalPoint>(75, 40);
|
||||||
|
|
||||||
let line: ExcalidrawLinearElement;
|
let line: ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
@ -414,17 +414,20 @@ describe("Test Linear Elements", () => {
|
||||||
|
|
||||||
it("should allow dragging lines from midpoints in between segments", async () => {
|
it("should allow dragging lines from midpoints in between segments", async () => {
|
||||||
// drag line via first segment midpoint
|
// drag line via first segment midpoint
|
||||||
drag(firstSegmentMidpoint, [
|
drag(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint,
|
||||||
firstSegmentMidpoint[1] + delta,
|
point(
|
||||||
]);
|
firstSegmentMidpoint[0] + delta,
|
||||||
|
firstSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(lastSegmentMidpoint, [
|
drag(
|
||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint,
|
||||||
lastSegmentMidpoint[1] + delta,
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
|
@ -472,10 +475,10 @@ describe("Test Linear Elements", () => {
|
||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
|
@ -513,10 +516,10 @@ describe("Test Linear Elements", () => {
|
||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
|
@ -551,10 +554,10 @@ describe("Test Linear Elements", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// dragging line from last segment midpoint
|
// dragging line from last segment midpoint
|
||||||
drag(lastSegmentMidpoint, [
|
drag(
|
||||||
lastSegmentMidpoint[0] + 50,
|
lastSegmentMidpoint,
|
||||||
lastSegmentMidpoint[1] + 50,
|
point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
|
||||||
]);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
|
@ -586,12 +589,14 @@ describe("Test Linear Elements", () => {
|
||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint: Point = [
|
const firstSegmentMidpoint = point<GlobalPoint>(
|
||||||
55.9697848965255, 47.442326230998205,
|
55.9697848965255,
|
||||||
];
|
47.442326230998205,
|
||||||
const lastSegmentMidpoint: Point = [
|
);
|
||||||
76.08587175006699, 43.294165939653226,
|
const lastSegmentMidpoint = point<GlobalPoint>(
|
||||||
];
|
76.08587175006699,
|
||||||
|
43.294165939653226,
|
||||||
|
);
|
||||||
let line: ExcalidrawLinearElement;
|
let line: ExcalidrawLinearElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -605,17 +610,20 @@ describe("Test Linear Elements", () => {
|
||||||
|
|
||||||
it("should allow dragging lines from midpoints in between segments", async () => {
|
it("should allow dragging lines from midpoints in between segments", async () => {
|
||||||
// drag line from first segment midpoint
|
// drag line from first segment midpoint
|
||||||
drag(firstSegmentMidpoint, [
|
drag(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint,
|
||||||
firstSegmentMidpoint[1] + delta,
|
point(
|
||||||
]);
|
firstSegmentMidpoint[0] + delta,
|
||||||
|
firstSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
// drag line from last segment midpoint
|
// drag line from last segment midpoint
|
||||||
drag(lastSegmentMidpoint, [
|
drag(
|
||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint,
|
||||||
lastSegmentMidpoint[1] + delta,
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
]);
|
);
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
);
|
);
|
||||||
|
@ -661,10 +669,10 @@ describe("Test Linear Elements", () => {
|
||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta));
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
|
@ -709,10 +717,10 @@ describe("Test Linear Elements", () => {
|
||||||
h.state,
|
h.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]);
|
||||||
|
|
||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta));
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
|
@ -741,10 +749,10 @@ describe("Test Linear Elements", () => {
|
||||||
it("should update all the midpoints when a point is deleted", async () => {
|
it("should update all the midpoints when a point is deleted", async () => {
|
||||||
const elementsMap = arrayToMap(h.elements);
|
const elementsMap = arrayToMap(h.elements);
|
||||||
|
|
||||||
drag(lastSegmentMidpoint, [
|
drag(
|
||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint,
|
||||||
lastSegmentMidpoint[1] + delta,
|
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta),
|
||||||
]);
|
);
|
||||||
expect(line.points.length).toEqual(4);
|
expect(line.points.length).toEqual(4);
|
||||||
|
|
||||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
|
@ -803,8 +811,11 @@ describe("Test Linear Elements", () => {
|
||||||
API.setSelectedElements([line]);
|
API.setSelectedElements([line]);
|
||||||
enterLineEditingMode(line, true);
|
enterLineEditingMode(line, true);
|
||||||
drag(
|
drag(
|
||||||
[line.points[0][0] + line.x, line.points[0][1] + line.y],
|
point(line.points[0][0] + line.x, line.points[0][1] + line.y),
|
||||||
[dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
|
point(
|
||||||
|
dragEndPositionOffset[0] + line.x,
|
||||||
|
dragEndPositionOffset[1] + line.y,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
|
@ -916,14 +927,18 @@ describe("Test Linear Elements", () => {
|
||||||
// This is the expected midpoint for line with round edge
|
// This is the expected midpoint for line with round edge
|
||||||
// hence hardcoding it so if later some bug is introduced
|
// hence hardcoding it so if later some bug is introduced
|
||||||
// this will fail and we can fix it
|
// this will fail and we can fix it
|
||||||
const firstSegmentMidpoint: Point = [
|
const firstSegmentMidpoint = point<GlobalPoint>(
|
||||||
55.9697848965255, 47.442326230998205,
|
55.9697848965255,
|
||||||
];
|
47.442326230998205,
|
||||||
|
);
|
||||||
// drag line from first segment midpoint
|
// drag line from first segment midpoint
|
||||||
drag(firstSegmentMidpoint, [
|
drag(
|
||||||
firstSegmentMidpoint[0] + delta,
|
firstSegmentMidpoint,
|
||||||
firstSegmentMidpoint[1] + delta,
|
point(
|
||||||
]);
|
firstSegmentMidpoint[0] + delta,
|
||||||
|
firstSegmentMidpoint[1] + delta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const position = LinearElementEditor.getBoundTextElementPosition(
|
const position = LinearElementEditor.getBoundTextElementPosition(
|
||||||
container,
|
container,
|
||||||
|
@ -1136,7 +1151,7 @@ describe("Test Linear Elements", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Drag from last point
|
// Drag from last point
|
||||||
drag(points[1], [points[1][0] + 300, points[1][1]]);
|
drag(points[1], point(points[1][0] + 300, points[1][1]));
|
||||||
|
|
||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
|
@ -1335,14 +1350,14 @@ describe("Test Linear Elements", () => {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
point: [line.points[0][0] + 10, line.points[0][1] + 10],
|
point: point(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: line.points.length - 1,
|
index: line.points.length - 1,
|
||||||
point: [
|
point: point(
|
||||||
line.points[line.points.length - 1][0] - 10,
|
line.points[line.points.length - 1][0] - 10,
|
||||||
line.points[line.points.length - 1][1] - 10,
|
line.points[line.points.length - 1][1] - 10,
|
||||||
],
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
new Map() as SceneElementsMap,
|
new Map() as SceneElementsMap,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type {
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type { Point } from "../types";
|
|
||||||
import type { Bounds } from "../element/bounds";
|
import type { Bounds } from "../element/bounds";
|
||||||
import { getElementPointsCoords } from "../element/bounds";
|
import { getElementPointsCoords } from "../element/bounds";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
|
@ -16,6 +15,8 @@ import { KEYS } from "../keys";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
|
import { point } from "../../math";
|
||||||
|
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
|
@ -217,18 +218,13 @@ describe("generic element", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
describe.each(["line", "freedraw"] as const)("%s element", (type) => {
|
||||||
const points: Record<typeof type, Point[]> = {
|
const points: Record<typeof type, LocalPoint[]> = {
|
||||||
line: [
|
line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)],
|
||||||
[0, 0],
|
|
||||||
[60, -20],
|
|
||||||
[20, 40],
|
|
||||||
[-40, 0],
|
|
||||||
],
|
|
||||||
freedraw: [
|
freedraw: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[-2.474600807561444, 41.021700699972],
|
point(-2.474600807561444, 41.021700699972),
|
||||||
[3.6627956000014024, 47.84174560617245],
|
point(3.6627956000014024, 47.84174560617245),
|
||||||
[40.495224145598115, 47.15909710753482],
|
point(40.495224145598115, 47.15909710753482),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -296,11 +292,11 @@ describe("arrow element", () => {
|
||||||
it("resizes with a label", async () => {
|
it("resizes with a label", async () => {
|
||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[40, 140],
|
point(40, 140),
|
||||||
[80, 60], // label's anchor
|
point(80, 60), // label's anchor
|
||||||
[180, 20],
|
point(180, 20),
|
||||||
[200, 120],
|
point(200, 120),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const label = await UI.editText(arrow, "Hello");
|
const label = await UI.editText(arrow, "Hello");
|
||||||
|
@ -694,24 +690,24 @@ describe("multiple selection", () => {
|
||||||
x: 60,
|
x: 60,
|
||||||
y: 40,
|
y: 40,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[-40, 40],
|
point(-40, 40),
|
||||||
[-60, 0],
|
point(-60, 0),
|
||||||
[0, -40],
|
point(0, -40),
|
||||||
[40, 20],
|
point(40, 20),
|
||||||
[0, 40],
|
point(0, 40),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const freedraw = UI.createElement("freedraw", {
|
const freedraw = UI.createElement("freedraw", {
|
||||||
x: 63.56072661326618,
|
x: 63.56072661326618,
|
||||||
y: 100,
|
y: 100,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[-43.56072661326618, 18.15048126846341],
|
point(-43.56072661326618, 18.15048126846341),
|
||||||
[-43.56072661326618, 29.041198460587566],
|
point(-43.56072661326618, 29.041198460587566),
|
||||||
[-38.115368017204105, 42.652452795512204],
|
point(-38.115368017204105, 42.652452795512204),
|
||||||
[-19.964886748740696, 66.24829266003775],
|
point(-19.964886748740696, 66.24829266003775),
|
||||||
[19.056612930986716, 77.1390098521619],
|
point(19.056612930986716, 77.1390098521619),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1050,13 +1046,13 @@ describe("multiple selection", () => {
|
||||||
x: 60,
|
x: 60,
|
||||||
y: 0,
|
y: 0,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[-40, 40],
|
point(-40, 40),
|
||||||
[-20, 60],
|
point(-20, 60),
|
||||||
[20, 20],
|
point(20, 20),
|
||||||
[40, 40],
|
point(40, 40),
|
||||||
[-20, 100],
|
point(-20, 100),
|
||||||
[-60, 60],
|
point(-60, 60),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import type {
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import type { Action } from "./actions/types";
|
import type { Action } from "./actions/types";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
|
||||||
import type { LinearElementEditor } from "./element/linearElementEditor";
|
import type { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
import type { SuggestedBinding } from "./element/binding";
|
import type { SuggestedBinding } from "./element/binding";
|
||||||
import type { ImportedDataState } from "./data/types";
|
import type { ImportedDataState } from "./data/types";
|
||||||
|
@ -43,8 +42,6 @@ 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";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
|
||||||
|
|
||||||
export type SocketId = string & { _brand: "SocketId" };
|
export type SocketId = string & { _brand: "SocketId" };
|
||||||
|
|
||||||
export type Collaborator = Readonly<{
|
export type Collaborator = Readonly<{
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { average } from "../math";
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
import type { EVENT } from "./constants";
|
import type { EVENT } from "./constants";
|
||||||
import {
|
import {
|
||||||
|
@ -992,10 +993,6 @@ export const isMemberOf = <T extends string>(
|
||||||
|
|
||||||
export const cloneJSON = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
export const cloneJSON = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
export const isFiniteNumber = (value: any): value is number => {
|
|
||||||
return typeof value === "number" && Number.isFinite(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateStable = <T extends any[] | Record<string, any>>(
|
export const updateStable = <T extends any[] | Record<string, any>>(
|
||||||
prevValue: T,
|
prevValue: T,
|
||||||
nextValue: T,
|
nextValue: T,
|
||||||
|
@ -1079,7 +1076,6 @@ export function addEventListener(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const average = (a: number, b: number) => (a + b) / 2;
|
|
||||||
export function getSvgPathFromStroke(points: number[][], closed = true) {
|
export function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||||
const len = points.length;
|
const len = points.length;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
|
||||||
import type { LineSegment } from "../utils";
|
import type { LineSegment } from "../utils";
|
||||||
import type { BoundingBox, Bounds } from "./element/bounds";
|
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||||
import { isBounds, isLineSegment } from "./element/typeChecks";
|
import { isBounds } from "./element/typeChecks";
|
||||||
import type { Point } from "./types";
|
|
||||||
|
|
||||||
// The global data holder to collect the debug operations
|
// The global data holder to collect the debug operations
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -15,18 +15,22 @@ declare global {
|
||||||
|
|
||||||
export type DebugElement = {
|
export type DebugElement = {
|
||||||
color: string;
|
color: string;
|
||||||
data: LineSegment;
|
data: LineSegment<GlobalPoint>;
|
||||||
permanent: boolean;
|
permanent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const debugDrawLine = (
|
export const debugDrawLine = (
|
||||||
segment: LineSegment | LineSegment[],
|
segment: LineSegment<GlobalPoint> | LineSegment<GlobalPoint>[],
|
||||||
opts?: {
|
opts?: {
|
||||||
color?: string;
|
color?: string;
|
||||||
permanent?: boolean;
|
permanent?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
(isLineSegment(segment) ? [segment] : segment).forEach((data) =>
|
const segments = (
|
||||||
|
isLineSegment(segment) ? [segment] : segment
|
||||||
|
) as LineSegment<GlobalPoint>[];
|
||||||
|
|
||||||
|
segments.forEach((data) =>
|
||||||
addToCurrentFrame({
|
addToCurrentFrame({
|
||||||
color: opts?.color ?? "red",
|
color: opts?.color ?? "red",
|
||||||
data,
|
data,
|
||||||
|
@ -36,7 +40,7 @@ export const debugDrawLine = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const debugDrawPoint = (
|
export const debugDrawPoint = (
|
||||||
point: Point,
|
p: GlobalPoint,
|
||||||
opts?: {
|
opts?: {
|
||||||
color?: string;
|
color?: string;
|
||||||
permanent?: boolean;
|
permanent?: boolean;
|
||||||
|
@ -47,20 +51,20 @@ export const debugDrawPoint = (
|
||||||
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||||
|
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
lineSegment(
|
||||||
[point[0] + xOffset - 10, point[1] + yOffset - 10],
|
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
|
||||||
[point[0] + xOffset + 10, point[1] + yOffset + 10],
|
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
||||||
],
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
permanent: opts?.permanent,
|
permanent: opts?.permanent,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
lineSegment(
|
||||||
[point[0] + xOffset - 10, point[1] + yOffset + 10],
|
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
|
||||||
[point[0] + xOffset + 10, point[1] + yOffset - 10],
|
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
||||||
],
|
),
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
permanent: opts?.permanent,
|
permanent: opts?.permanent,
|
||||||
|
@ -78,22 +82,22 @@ export const debugDrawBoundingBox = (
|
||||||
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
|
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
[
|
lineSegment(
|
||||||
[bbox.minX, bbox.minY],
|
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
[bbox.maxX, bbox.minY],
|
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox.maxX, bbox.minY],
|
point<GlobalPoint>(bbox.maxX, bbox.minY),
|
||||||
[bbox.maxX, bbox.maxY],
|
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox.maxX, bbox.maxY],
|
point<GlobalPoint>(bbox.maxX, bbox.maxY),
|
||||||
[bbox.minX, bbox.maxY],
|
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox.minX, bbox.maxY],
|
point<GlobalPoint>(bbox.minX, bbox.maxY),
|
||||||
[bbox.minX, bbox.minY],
|
point<GlobalPoint>(bbox.minX, bbox.minY),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "cyan",
|
color: opts?.color ?? "cyan",
|
||||||
|
@ -113,22 +117,22 @@ export const debugDrawBounds = (
|
||||||
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||||
debugDrawLine(
|
debugDrawLine(
|
||||||
[
|
[
|
||||||
[
|
lineSegment(
|
||||||
[bbox[0], bbox[1]],
|
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
[bbox[2], bbox[1]],
|
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox[2], bbox[1]],
|
point<GlobalPoint>(bbox[2], bbox[1]),
|
||||||
[bbox[2], bbox[3]],
|
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox[2], bbox[3]],
|
point<GlobalPoint>(bbox[2], bbox[3]),
|
||||||
[bbox[0], bbox[3]],
|
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
],
|
),
|
||||||
[
|
lineSegment(
|
||||||
[bbox[0], bbox[3]],
|
point<GlobalPoint>(bbox[0], bbox[3]),
|
||||||
[bbox[0], bbox[1]],
|
point<GlobalPoint>(bbox[0], bbox[1]),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "green",
|
color: opts?.color ?? "green",
|
||||||
|
|
0
packages/math/CHANGELOG.md
Normal file
0
packages/math/CHANGELOG.md
Normal file
21
packages/math/README.md
Normal file
21
packages/math/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# @excalidraw/math
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/math
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/math
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/math
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
47
packages/math/angle.ts
Normal file
47
packages/math/angle.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import type {
|
||||||
|
Degrees,
|
||||||
|
GlobalPoint,
|
||||||
|
LocalPoint,
|
||||||
|
PolarCoords,
|
||||||
|
Radians,
|
||||||
|
} from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
|
||||||
|
// TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI
|
||||||
|
export const normalizeRadians = (angle: Radians): Radians => {
|
||||||
|
if (angle < 0) {
|
||||||
|
return (angle + 2 * Math.PI) as Radians;
|
||||||
|
}
|
||||||
|
if (angle >= 2 * Math.PI) {
|
||||||
|
return (angle - 2 * Math.PI) as Radians;
|
||||||
|
}
|
||||||
|
return angle;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the polar coordinates for the given cartesian point represented by
|
||||||
|
* (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 function degreesToRadians(degrees: Degrees): Radians {
|
||||||
|
return ((degrees * Math.PI) / 180) as Radians;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function radiansToDegrees(degrees: Radians): Degrees {
|
||||||
|
return ((degrees * 180) / Math.PI) as Degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the provided angle is a right angle.
|
||||||
|
*
|
||||||
|
* @param rads The angle to measure
|
||||||
|
* @returns TRUE if the provided angle is a right angle
|
||||||
|
*/
|
||||||
|
export function isRightAngleRads(rads: Radians): boolean {
|
||||||
|
return Math.abs(Math.sin(2 * rads)) < PRECISION;
|
||||||
|
}
|
41
packages/math/arc.test.ts
Normal file
41
packages/math/arc.test.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { isPointOnSymmetricArc } from "./arc";
|
||||||
|
import { point } from "./point";
|
||||||
|
|
||||||
|
describe("point on arc", () => {
|
||||||
|
it("should detect point on simple arc", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
point(0.92291667, 0.385),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("should not detect point outside of a simple arc", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
point(-0.92291667, 0.385),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
it("should not detect point with good angle but incorrect radius", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
point(-0.5, 0.5),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
20
packages/math/arc.ts
Normal file
20
packages/math/arc.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { cartesian2Polar } from "./angle";
|
||||||
|
import type { GlobalPoint, LocalPoint, SymmetricArc } 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>(
|
||||||
|
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
||||||
|
point: P,
|
||||||
|
): boolean => {
|
||||||
|
const [radius, angle] = cartesian2Polar(point);
|
||||||
|
|
||||||
|
return startAngle < endAngle
|
||||||
|
? Math.abs(radius - arcRadius) < PRECISION &&
|
||||||
|
startAngle <= angle &&
|
||||||
|
endAngle >= angle
|
||||||
|
: startAngle <= angle || endAngle >= angle;
|
||||||
|
};
|
223
packages/math/curve.ts
Normal file
223
packages/math/curve.ts
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
import { point, pointRotateRads } from "./point";
|
||||||
|
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @param c
|
||||||
|
* @param d
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function curve<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
c: Point,
|
||||||
|
d: Point,
|
||||||
|
) {
|
||||||
|
return [a, b, c, d] as Curve<Point>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const curveRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
curve: Curve<Point>,
|
||||||
|
angle: Radians,
|
||||||
|
origin: Point,
|
||||||
|
) => {
|
||||||
|
return curve.map((p) => pointRotateRads(p, origin, angle));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param pointsIn
|
||||||
|
* @param curveTightness
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
||||||
|
pointsIn: readonly Point[],
|
||||||
|
curveTightness = 0,
|
||||||
|
): Point[] {
|
||||||
|
const len = pointsIn.length;
|
||||||
|
if (len < 3) {
|
||||||
|
throw new Error("A curve must have at least three points.");
|
||||||
|
}
|
||||||
|
const out: Point[] = [];
|
||||||
|
if (len === 3) {
|
||||||
|
out.push(
|
||||||
|
point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||||
|
point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||||
|
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||||
|
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const points: Point[] = [];
|
||||||
|
points.push(pointsIn[0], pointsIn[0]);
|
||||||
|
for (let i = 1; i < pointsIn.length; i++) {
|
||||||
|
points.push(pointsIn[i]);
|
||||||
|
if (i === pointsIn.length - 1) {
|
||||||
|
points.push(pointsIn[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const b: Point[] = [];
|
||||||
|
const s = 1 - curveTightness;
|
||||||
|
out.push(point(points[0][0], points[0][1]));
|
||||||
|
for (let i = 1; i + 2 < points.length; i++) {
|
||||||
|
const cachedVertArray = points[i];
|
||||||
|
b[0] = point(cachedVertArray[0], cachedVertArray[1]);
|
||||||
|
b[1] = point(
|
||||||
|
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
||||||
|
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
||||||
|
);
|
||||||
|
b[2] = point(
|
||||||
|
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
||||||
|
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
||||||
|
);
|
||||||
|
b[3] = point(points[i + 1][0], points[i + 1][1]);
|
||||||
|
out.push(b[1], b[2], b[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param t
|
||||||
|
* @param controlPoints
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
t: number,
|
||||||
|
controlPoints: Curve<Point>,
|
||||||
|
): Point => {
|
||||||
|
const [p0, p1, p2, p3] = controlPoints;
|
||||||
|
|
||||||
|
const x =
|
||||||
|
Math.pow(1 - t, 3) * p0[0] +
|
||||||
|
3 * Math.pow(1 - t, 2) * t * p1[0] +
|
||||||
|
3 * (1 - t) * Math.pow(t, 2) * p2[0] +
|
||||||
|
Math.pow(t, 3) * p3[0];
|
||||||
|
|
||||||
|
const y =
|
||||||
|
Math.pow(1 - t, 3) * p0[1] +
|
||||||
|
3 * Math.pow(1 - t, 2) * t * p1[1] +
|
||||||
|
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
||||||
|
Math.pow(t, 3) * p3[1];
|
||||||
|
|
||||||
|
return point(x, y);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* @param controlPoints
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
controlPoints: Curve<Point>,
|
||||||
|
) => {
|
||||||
|
// Calculate the closest point on the Bezier curve to the given point
|
||||||
|
const t = findClosestParameter(point, controlPoints);
|
||||||
|
|
||||||
|
// Calculate the coordinates of the closest point on the curve
|
||||||
|
const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
|
||||||
|
|
||||||
|
// Calculate the distance between the given point and the closest point on the curve
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
(point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const solveCubic = (a: number, b: number, c: number, d: number) => {
|
||||||
|
// This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
|
||||||
|
const roots: number[] = [];
|
||||||
|
|
||||||
|
const discriminant =
|
||||||
|
18 * a * b * c * d -
|
||||||
|
4 * Math.pow(b, 3) * d +
|
||||||
|
Math.pow(b, 2) * Math.pow(c, 2) -
|
||||||
|
4 * a * Math.pow(c, 3) -
|
||||||
|
27 * Math.pow(a, 2) * Math.pow(d, 2);
|
||||||
|
|
||||||
|
if (discriminant >= 0) {
|
||||||
|
const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
|
||||||
|
const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
|
||||||
|
|
||||||
|
const root1 = (-b - C - D) / (3 * a);
|
||||||
|
const root2 = (-b + (C + D) / 2) / (3 * a);
|
||||||
|
const root3 = (-b + (C + D) / 2) / (3 * a);
|
||||||
|
|
||||||
|
roots.push(root1, root2, root3);
|
||||||
|
} else {
|
||||||
|
const realPart = -b / (3 * a);
|
||||||
|
|
||||||
|
const root1 =
|
||||||
|
2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
|
||||||
|
const root2 =
|
||||||
|
2 *
|
||||||
|
Math.sqrt(-b / (3 * a)) *
|
||||||
|
Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
|
||||||
|
const root3 =
|
||||||
|
2 *
|
||||||
|
Math.sqrt(-b / (3 * a)) *
|
||||||
|
Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
|
||||||
|
|
||||||
|
roots.push(root1, root2, root3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findClosestParameter = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
controlPoints: Curve<Point>,
|
||||||
|
) => {
|
||||||
|
// This function finds the parameter t that minimizes the distance between the point
|
||||||
|
// and any point on the cubic Bezier curve.
|
||||||
|
|
||||||
|
const [p0, p1, p2, p3] = controlPoints;
|
||||||
|
|
||||||
|
// Use the direct formula to find the parameter t
|
||||||
|
const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
|
||||||
|
const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
|
||||||
|
const c = 3 * p1[0] - 3 * p0[0];
|
||||||
|
const d = p0[0] - point[0];
|
||||||
|
|
||||||
|
const rootsX = solveCubic(a, b, c, d);
|
||||||
|
|
||||||
|
// Do the same for the y-coordinate
|
||||||
|
const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
|
||||||
|
const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
|
||||||
|
const g = 3 * p1[1] - 3 * p0[1];
|
||||||
|
const h = p0[1] - point[1];
|
||||||
|
|
||||||
|
const rootsY = solveCubic(e, f, g, h);
|
||||||
|
|
||||||
|
// Select the real root that is between 0 and 1 (inclusive)
|
||||||
|
const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
|
||||||
|
const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
|
||||||
|
|
||||||
|
if (validRootsX.length === 0 || validRootsY.length === 0) {
|
||||||
|
// No valid roots found, use the midpoint as a fallback
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose the parameter t that minimizes the distance
|
||||||
|
let minDistance = Infinity;
|
||||||
|
let closestT = 0;
|
||||||
|
|
||||||
|
for (const rootX of validRootsX) {
|
||||||
|
for (const rootY of validRootsY) {
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
(rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
|
||||||
|
);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestT = (rootX + rootY) / 2; // Use the average for a smoother result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestT;
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import * as GA from "../ga";
|
import * as GA from "./ga";
|
||||||
import { point, toString, direction, offset } from "../ga";
|
import { point, toString, direction, offset } from "./ga";
|
||||||
import * as GAPoint from "../gapoints";
|
import * as GAPoint from "./gapoints";
|
||||||
import * as GALine from "../galines";
|
import * as GALine from "./galines";
|
||||||
import * as GATransform from "../gatransforms";
|
import * as GATransform from "./gatransforms";
|
||||||
|
|
||||||
describe("geometric algebra", () => {
|
describe("geometric algebra", () => {
|
||||||
describe("points", () => {
|
describe("points", () => {
|
12
packages/math/index.ts
Normal file
12
packages/math/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export * from "./arc";
|
||||||
|
export * from "./angle";
|
||||||
|
export * from "./curve";
|
||||||
|
export * from "./line";
|
||||||
|
export * from "./point";
|
||||||
|
export * from "./polygon";
|
||||||
|
export * from "./range";
|
||||||
|
export * from "./segment";
|
||||||
|
export * from "./triangle";
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./vector";
|
||||||
|
export * from "./utils";
|
52
packages/math/line.ts
Normal file
52
packages/math/line.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { pointCenter, pointRotateRads } from "./point";
|
||||||
|
import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a line from two points.
|
||||||
|
*
|
||||||
|
* @param points The two points lying on the line
|
||||||
|
* @returns The line on which the points lie
|
||||||
|
*/
|
||||||
|
export function line<P extends GlobalPoint | LocalPoint>(a: P, b: P): Line<P> {
|
||||||
|
return [a, b] as Line<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient point creation from an array of two points.
|
||||||
|
*
|
||||||
|
* @param param0 The array with the two points to convert to a line
|
||||||
|
* @returns The created line
|
||||||
|
*/
|
||||||
|
export function lineFromPointPair<P extends GlobalPoint | LocalPoint>([a, b]: [
|
||||||
|
P,
|
||||||
|
P,
|
||||||
|
]): Line<P> {
|
||||||
|
return line(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*
|
||||||
|
* @param pointArray
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function lineFromPointArray<P extends GlobalPoint | LocalPoint>(
|
||||||
|
pointArray: P[],
|
||||||
|
): Line<P> | undefined {
|
||||||
|
return pointArray.length === 2
|
||||||
|
? line<P>(pointArray[0], pointArray[1])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the coordinates resulting from rotating the given line about an origin by an angle in degrees
|
||||||
|
// note that when the origin is not given, the midpoint of the given line is used as the origin
|
||||||
|
export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
l: Line<Point>,
|
||||||
|
angle: Radians,
|
||||||
|
origin?: Point,
|
||||||
|
): Line<Point> => {
|
||||||
|
return line(
|
||||||
|
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
|
||||||
|
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
||||||
|
);
|
||||||
|
};
|
61
packages/math/package.json
Normal file
61
packages/math/package.json
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "@excalidraw/math",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": "./dist/utils/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw math functions",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-math",
|
||||||
|
"math",
|
||||||
|
"vector",
|
||||||
|
"algebra",
|
||||||
|
"2d"
|
||||||
|
],
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all",
|
||||||
|
"not safari < 12",
|
||||||
|
"not kaios <= 2.5",
|
||||||
|
"not edge < 79",
|
||||||
|
"not chrome < 70",
|
||||||
|
"not and_uc < 13",
|
||||||
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"dependencies": {
|
||||||
|
"@excalidraw/utils": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types",
|
||||||
|
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
||||||
|
"pack": "yarn build:umd && yarn pack"
|
||||||
|
}
|
||||||
|
}
|
24
packages/math/point.test.ts
Normal file
24
packages/math/point.test.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { point, pointRotateRads } from "./point";
|
||||||
|
import type { Radians } from "./types";
|
||||||
|
|
||||||
|
describe("rotate", () => {
|
||||||
|
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||||
|
const x1 = 10;
|
||||||
|
const y1 = 20;
|
||||||
|
const x2 = 20;
|
||||||
|
const y2 = 30;
|
||||||
|
const angle = (Math.PI / 2) as Radians;
|
||||||
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
|
point(x1, y1),
|
||||||
|
point(x2, y2),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
|
expect([rotatedX, rotatedY]).toEqual([30, 20]);
|
||||||
|
const res2 = pointRotateRads(
|
||||||
|
point(rotatedX, rotatedY),
|
||||||
|
point(x2, y2),
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
expect(res2).toEqual([x1, x2]);
|
||||||
|
});
|
||||||
|
});
|
257
packages/math/point.ts
Normal file
257
packages/math/point.ts
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
import { degreesToRadians } from "./angle";
|
||||||
|
import type {
|
||||||
|
LocalPoint,
|
||||||
|
GlobalPoint,
|
||||||
|
Radians,
|
||||||
|
Degrees,
|
||||||
|
Vector,
|
||||||
|
} from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
import { vectorFromPoint, vectorScale } from "./vector";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a properly typed Point instance from the X and Y coordinates.
|
||||||
|
*
|
||||||
|
* @param x The X coordinate
|
||||||
|
* @param y The Y coordinate
|
||||||
|
* @returns The branded and created point
|
||||||
|
*/
|
||||||
|
export function point<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): Point {
|
||||||
|
return [x, y] as Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts and remaps an array containing a pair of numbers to Point.
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
return numberArray.length === 2
|
||||||
|
? point<Point>(numberArray[0], numberArray[1])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts and remaps a pair of numbers to Point.
|
||||||
|
*
|
||||||
|
* @param pair A number pair to convert to Point
|
||||||
|
* @returns The point instance
|
||||||
|
*/
|
||||||
|
export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
pair: [number, number],
|
||||||
|
): Point {
|
||||||
|
return pair as Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a vector to a point.
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
return v as unknown as P;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the provided value has the shape of a Point.
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
return (
|
||||||
|
Array.isArray(p) &&
|
||||||
|
p.length === 2 &&
|
||||||
|
typeof p[0] === "number" &&
|
||||||
|
!isNaN(p[0]) &&
|
||||||
|
typeof p[1] === "number" &&
|
||||||
|
!isNaN(p[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two points coordinate-by-coordinate and if
|
||||||
|
* they are closer than INVERSE_PRECISION it returns TRUE.
|
||||||
|
*
|
||||||
|
* @param a Point The first point to compare
|
||||||
|
* @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 {
|
||||||
|
const abs = Math.abs;
|
||||||
|
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roate a point by [angle] radians.
|
||||||
|
*
|
||||||
|
* @param point The point to rotate
|
||||||
|
* @param center The point to rotate around, the center point
|
||||||
|
* @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 {
|
||||||
|
return point(
|
||||||
|
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||||
|
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roate a point by [angle] degree.
|
||||||
|
*
|
||||||
|
* @param point The point to rotate
|
||||||
|
* @param center The point to rotate around, the center point
|
||||||
|
* @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 {
|
||||||
|
return pointRotateRads(point, center, degreesToRadians(angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate a point by a vector.
|
||||||
|
*
|
||||||
|
* WARNING: This is not for translating Excalidraw element points!
|
||||||
|
* You need to account for rotation on base coordinates
|
||||||
|
* on your own.
|
||||||
|
* CONSIDER USING AN APPROPRIATE ELEMENT-AWARE TRANSLATE!
|
||||||
|
*
|
||||||
|
* @param p The point to apply the translation on
|
||||||
|
* @param v The vector to translate by
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
// 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,
|
||||||
|
>(p: From, v: Vector = [0, 0] as Vector): To {
|
||||||
|
return point(p[0] + v[0], p[1] + v[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the center point at equal distance from both points.
|
||||||
|
*
|
||||||
|
* @param a One of the points to create the middle point for
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add together two points by their coordinates like you'd apply a translation
|
||||||
|
* to a point by a vector.
|
||||||
|
*
|
||||||
|
* @param a One point to act as a basis
|
||||||
|
* @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 {
|
||||||
|
return point(a[0] + b[0], a[1] + b[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtract a point from another point like you'd translate a point by an
|
||||||
|
* invese vector.
|
||||||
|
*
|
||||||
|
* @param a The point to translate
|
||||||
|
* @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 {
|
||||||
|
return point(a[0] - b[0], a[1] - b[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the distance between two points.
|
||||||
|
*
|
||||||
|
* @param a First point
|
||||||
|
* @param b Second point
|
||||||
|
* @returns The euclidean distance between the two points.
|
||||||
|
*/
|
||||||
|
export function pointDistance<P extends LocalPoint | GlobalPoint>(
|
||||||
|
a: P,
|
||||||
|
b: P,
|
||||||
|
): number {
|
||||||
|
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the squared distance between two points.
|
||||||
|
*
|
||||||
|
* Note: Use this if you only compare distances, it saves a square root.
|
||||||
|
*
|
||||||
|
* @param a First point
|
||||||
|
* @param b Second point
|
||||||
|
* @returns The euclidean distance between the two points.
|
||||||
|
*/
|
||||||
|
export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
|
||||||
|
a: P,
|
||||||
|
b: P,
|
||||||
|
): number {
|
||||||
|
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale a point from a given origin by the multiplier.
|
||||||
|
*
|
||||||
|
* @param p The point to scale
|
||||||
|
* @param mid The origin to scale from
|
||||||
|
* @param multiplier The scaling factor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pointScaleFromOrigin = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
mid: P,
|
||||||
|
multiplier: number,
|
||||||
|
) => pointTranslate(mid, vectorScale(vectorFromPoint(p, mid), multiplier));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether `q` lies inside the segment/rectangle defined by `p` and `r`.
|
||||||
|
* This is an approximation to "does `q` lie on a segment `pr`" check.
|
||||||
|
*
|
||||||
|
* @param p The first point to compare against
|
||||||
|
* @param q The actual point this function checks whether is in between
|
||||||
|
* @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>(
|
||||||
|
p: P,
|
||||||
|
q: P,
|
||||||
|
r: P,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
q[0] <= Math.max(p[0], r[0]) &&
|
||||||
|
q[0] >= Math.min(p[0], r[0]) &&
|
||||||
|
q[1] <= Math.max(p[1], r[1]) &&
|
||||||
|
q[1] >= Math.min(p[1], r[1])
|
||||||
|
);
|
||||||
|
};
|
72
packages/math/polygon.ts
Normal file
72
packages/math/polygon.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { pointsEqual } from "./point";
|
||||||
|
import { lineSegment, pointOnLineSegment } from "./segment";
|
||||||
|
import type { GlobalPoint, LocalPoint, Polygon } from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
|
||||||
|
export function polygon<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
...points: Point[]
|
||||||
|
) {
|
||||||
|
return polygonClose(points) as Polygon<Point>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function polygonFromPoints<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
points: Point[],
|
||||||
|
) {
|
||||||
|
return polygonClose(points) as Polygon<Point>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
polygon: Polygon<Point>,
|
||||||
|
) => {
|
||||||
|
const x = point[0];
|
||||||
|
const y = point[1];
|
||||||
|
let inside = false;
|
||||||
|
|
||||||
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
|
const xi = polygon[i][0];
|
||||||
|
const yi = polygon[i][1];
|
||||||
|
const xj = polygon[j][0];
|
||||||
|
const yj = polygon[j][1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
((yi > y && yj <= y) || (yi <= y && yj > y)) &&
|
||||||
|
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
||||||
|
) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
p: Point,
|
||||||
|
poly: Polygon<Point>,
|
||||||
|
threshold = PRECISION,
|
||||||
|
) => {
|
||||||
|
let on = false;
|
||||||
|
|
||||||
|
for (let i = 0, l = poly.length - 1; i < l; i++) {
|
||||||
|
if (pointOnLineSegment(p, lineSegment(poly[i], poly[i + 1]), threshold)) {
|
||||||
|
on = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return on;
|
||||||
|
};
|
||||||
|
|
||||||
|
function polygonClose<Point extends LocalPoint | GlobalPoint>(
|
||||||
|
polygon: Point[],
|
||||||
|
) {
|
||||||
|
return polygonIsClosed(polygon)
|
||||||
|
? polygon
|
||||||
|
: ([...polygon, polygon[0]] as Polygon<Point>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function polygonIsClosed<Point extends LocalPoint | GlobalPoint>(
|
||||||
|
polygon: Point[],
|
||||||
|
) {
|
||||||
|
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
||||||
|
}
|
51
packages/math/range.test.ts
Normal file
51
packages/math/range.test.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { rangeInclusive, rangeIntersection, rangesOverlap } from "./range";
|
||||||
|
|
||||||
|
describe("range overlap", () => {
|
||||||
|
const range1_4 = rangeInclusive(1, 4);
|
||||||
|
|
||||||
|
it("should overlap when range a contains range b", () => {
|
||||||
|
expect(rangesOverlap(range1_4, rangeInclusive(2, 3))).toBe(true);
|
||||||
|
expect(rangesOverlap(range1_4, range1_4)).toBe(true);
|
||||||
|
expect(rangesOverlap(range1_4, rangeInclusive(1, 3))).toBe(true);
|
||||||
|
expect(rangesOverlap(range1_4, rangeInclusive(2, 4))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should overlap when range b contains range a", () => {
|
||||||
|
expect(rangesOverlap(rangeInclusive(2, 3), range1_4)).toBe(true);
|
||||||
|
expect(rangesOverlap(rangeInclusive(1, 3), range1_4)).toBe(true);
|
||||||
|
expect(rangesOverlap(rangeInclusive(2, 4), range1_4)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should overlap when range a and b intersect", () => {
|
||||||
|
expect(rangesOverlap(range1_4, rangeInclusive(2, 5))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("range intersection", () => {
|
||||||
|
const range1_4 = rangeInclusive(1, 4);
|
||||||
|
|
||||||
|
it("should intersect completely with itself", () => {
|
||||||
|
expect(rangeIntersection(range1_4, range1_4)).toEqual(range1_4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should intersect irrespective of order", () => {
|
||||||
|
expect(rangeIntersection(range1_4, rangeInclusive(2, 3))).toEqual([2, 3]);
|
||||||
|
expect(rangeIntersection(rangeInclusive(2, 3), range1_4)).toEqual([2, 3]);
|
||||||
|
expect(rangeIntersection(range1_4, rangeInclusive(3, 5))).toEqual(
|
||||||
|
rangeInclusive(3, 4),
|
||||||
|
);
|
||||||
|
expect(rangeIntersection(rangeInclusive(3, 5), range1_4)).toEqual(
|
||||||
|
rangeInclusive(3, 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should intersect at the edge", () => {
|
||||||
|
expect(rangeIntersection(range1_4, rangeInclusive(4, 5))).toEqual(
|
||||||
|
rangeInclusive(4, 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not intersect", () => {
|
||||||
|
expect(rangeIntersection(range1_4, rangeInclusive(5, 7))).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
82
packages/math/range.ts
Normal file
82
packages/math/range.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { toBrandedType } from "../excalidraw/utils";
|
||||||
|
import type { InclusiveRange } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an inclusive range from the two numbers provided.
|
||||||
|
*
|
||||||
|
* @param start Start of the range
|
||||||
|
* @param end End of the range
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function rangeInclusive(start: number, end: number): InclusiveRange {
|
||||||
|
return toBrandedType<InclusiveRange>([start, end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a number pair into an inclusive range.
|
||||||
|
*
|
||||||
|
* @param pair The number pair to convert to an inclusive range
|
||||||
|
* @returns The new inclusive range
|
||||||
|
*/
|
||||||
|
export function rangeInclusiveFromPair(pair: [start: number, end: number]) {
|
||||||
|
return toBrandedType<InclusiveRange>(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two ranges, return if the two ranges overlap with each other e.g.
|
||||||
|
* [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5].
|
||||||
|
*
|
||||||
|
* @param param0 One of the ranges to compare
|
||||||
|
* @param param1 The other range to compare against
|
||||||
|
* @returns TRUE if the ranges overlap
|
||||||
|
*/
|
||||||
|
export const rangesOverlap = (
|
||||||
|
[a0, a1]: InclusiveRange,
|
||||||
|
[b0, b1]: InclusiveRange,
|
||||||
|
): boolean => {
|
||||||
|
if (a0 <= b0) {
|
||||||
|
return a1 >= b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a0 >= b0) {
|
||||||
|
return b1 >= a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two ranges,return ther intersection of the two ranges if any e.g. the
|
||||||
|
* intersection of [1, 3] and [2, 4] is [2, 3].
|
||||||
|
*
|
||||||
|
* @param param0 The first range to compare
|
||||||
|
* @param param1 The second range to compare
|
||||||
|
* @returns The inclusive range intersection or NULL if no intersection
|
||||||
|
*/
|
||||||
|
export const rangeIntersection = (
|
||||||
|
[a0, a1]: InclusiveRange,
|
||||||
|
[b0, b1]: InclusiveRange,
|
||||||
|
): InclusiveRange | null => {
|
||||||
|
const rangeStart = Math.max(a0, b0);
|
||||||
|
const rangeEnd = Math.min(a1, b1);
|
||||||
|
|
||||||
|
if (rangeStart <= rangeEnd) {
|
||||||
|
return toBrandedType<InclusiveRange>([rangeStart, rangeEnd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a value is inside a range.
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
* @param range The range
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const rangeIncludesValue = (
|
||||||
|
value: number,
|
||||||
|
[min, max]: InclusiveRange,
|
||||||
|
): boolean => {
|
||||||
|
return value >= min && value <= max;
|
||||||
|
};
|
158
packages/math/segment.ts
Normal file
158
packages/math/segment.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import {
|
||||||
|
isPoint,
|
||||||
|
pointCenter,
|
||||||
|
pointFromVector,
|
||||||
|
pointRotateRads,
|
||||||
|
} from "./point";
|
||||||
|
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
import {
|
||||||
|
vectorAdd,
|
||||||
|
vectorCross,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
|
vectorSubtract,
|
||||||
|
} from "./vector";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a line segment from two points.
|
||||||
|
*
|
||||||
|
* @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>(
|
||||||
|
a: P,
|
||||||
|
b: P,
|
||||||
|
): LineSegment<P> {
|
||||||
|
return [a, b] as LineSegment<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lineSegmentFromPointArray<P extends GlobalPoint | LocalPoint>(
|
||||||
|
pointArray: P[],
|
||||||
|
): LineSegment<P> | undefined {
|
||||||
|
return pointArray.length === 2
|
||||||
|
? lineSegment<P>(pointArray[0], pointArray[1])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param segment
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isLineSegment = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
segment: unknown,
|
||||||
|
): segment is LineSegment<Point> =>
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
segment.length === 2 &&
|
||||||
|
isPoint(segment[0]) &&
|
||||||
|
isPoint(segment[0]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the coordinates resulting from rotating the given line about an origin by an angle in radians
|
||||||
|
* note that when the origin is not given, the midpoint of the given line is used as the origin.
|
||||||
|
*
|
||||||
|
* @param l
|
||||||
|
* @param angle
|
||||||
|
* @param origin
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
l: LineSegment<Point>,
|
||||||
|
angle: Radians,
|
||||||
|
origin?: Point,
|
||||||
|
): LineSegment<Point> => {
|
||||||
|
return lineSegment(
|
||||||
|
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
|
||||||
|
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the point two line segments with a definite start and end point
|
||||||
|
* intersect at.
|
||||||
|
*/
|
||||||
|
export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
a: Readonly<LineSegment<Point>>,
|
||||||
|
b: Readonly<LineSegment<Point>>,
|
||||||
|
): Point | null => {
|
||||||
|
const a0 = vectorFromPoint(a[0]);
|
||||||
|
const a1 = vectorFromPoint(a[1]);
|
||||||
|
const b0 = vectorFromPoint(b[0]);
|
||||||
|
const b1 = vectorFromPoint(b[1]);
|
||||||
|
const r = vectorSubtract(a1, a0);
|
||||||
|
const s = vectorSubtract(b1, b0);
|
||||||
|
const denominator = vectorCross(r, s);
|
||||||
|
|
||||||
|
if (denominator === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i = vectorSubtract(vectorFromPoint(b[0]), vectorFromPoint(a[0]));
|
||||||
|
const u = vectorCross(i, r) / denominator;
|
||||||
|
const t = vectorCross(i, s) / denominator;
|
||||||
|
|
||||||
|
if (u === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = vectorAdd(a0, vectorScale(r, t));
|
||||||
|
|
||||||
|
if (t >= 0 && t < 1 && u >= 0 && u < 1) {
|
||||||
|
return pointFromVector<Point>(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
line: LineSegment<Point>,
|
||||||
|
threshold = PRECISION,
|
||||||
|
) => {
|
||||||
|
const distance = distanceToLineSegment(point, line);
|
||||||
|
|
||||||
|
if (distance === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance < threshold;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
line: LineSegment<Point>,
|
||||||
|
) => {
|
||||||
|
const [x, y] = point;
|
||||||
|
const [[x1, y1], [x2, y2]] = line;
|
||||||
|
|
||||||
|
const A = x - x1;
|
||||||
|
const B = y - y1;
|
||||||
|
const C = x2 - x1;
|
||||||
|
const D = y2 - y1;
|
||||||
|
|
||||||
|
const dot = A * C + B * D;
|
||||||
|
const len_sq = C * C + D * D;
|
||||||
|
let param = -1;
|
||||||
|
if (len_sq !== 0) {
|
||||||
|
param = dot / len_sq;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xx;
|
||||||
|
let yy;
|
||||||
|
|
||||||
|
if (param < 0) {
|
||||||
|
xx = x1;
|
||||||
|
yy = y1;
|
||||||
|
} else if (param > 1) {
|
||||||
|
xx = x2;
|
||||||
|
yy = y2;
|
||||||
|
} else {
|
||||||
|
xx = x1 + param * C;
|
||||||
|
yy = y1 + param * D;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = x - xx;
|
||||||
|
const dy = y - yy;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
};
|
28
packages/math/triangle.ts
Normal file
28
packages/math/triangle.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import type { GlobalPoint, LocalPoint, Triangle } from "./types";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a point lies inside a triangle. This function
|
||||||
|
* will return FALSE if the point lies exactly on the sides
|
||||||
|
* of the triangle.
|
||||||
|
*
|
||||||
|
* @param triangle The triangle to test the point for
|
||||||
|
* @param p The point to test whether is in the triangle
|
||||||
|
* @returns TRUE if the point is inside of the triangle
|
||||||
|
*/
|
||||||
|
export function triangleIncludesPoint<P extends GlobalPoint | LocalPoint>(
|
||||||
|
[a, b, c]: Triangle<P>,
|
||||||
|
p: P,
|
||||||
|
): boolean {
|
||||||
|
const triangleSign = (p1: P, p2: P, p3: P) =>
|
||||||
|
(p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]);
|
||||||
|
const d1 = triangleSign(p, a, b);
|
||||||
|
const d2 = triangleSign(p, b, c);
|
||||||
|
const d3 = triangleSign(p, c, a);
|
||||||
|
|
||||||
|
const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
|
||||||
|
const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
|
||||||
|
|
||||||
|
return !(has_neg && has_pos);
|
||||||
|
}
|
130
packages/math/types.ts
Normal file
130
packages/math/types.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// Measurements
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By definition one radian is the angle subtended at the centre
|
||||||
|
* of a circle by an arc that is equal in length to the radius.
|
||||||
|
*/
|
||||||
|
export type Radians = number & { _brand: "excalimath__radian" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An angle measurement of a plane angle in which one full
|
||||||
|
* rotation is 360 degrees.
|
||||||
|
*/
|
||||||
|
export type Degrees = number & { _brand: "excalimath_degree" };
|
||||||
|
|
||||||
|
//
|
||||||
|
// Range
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number range which includes the start and end numbers in the range.
|
||||||
|
*/
|
||||||
|
export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" };
|
||||||
|
|
||||||
|
//
|
||||||
|
// Point
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a 2D position in world or canvas space. A
|
||||||
|
* global coordinate.
|
||||||
|
*/
|
||||||
|
export type GlobalPoint = [x: number, y: number] & {
|
||||||
|
_brand: "excalimath__globalpoint";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a 2D position in whatever local space it's
|
||||||
|
* needed. A local coordinate.
|
||||||
|
*/
|
||||||
|
export type LocalPoint = [x: number, y: number] & {
|
||||||
|
_brand: "excalimath__localpoint";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A line is an infinitely long object with no width, depth, or curvature.
|
||||||
|
*/
|
||||||
|
export type Line<P extends GlobalPoint | LocalPoint> = [p: P, q: P] & {
|
||||||
|
_brand: "excalimath_line";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In geometry, a line segment is a part of a straight
|
||||||
|
* 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] & {
|
||||||
|
_brand: "excalimath_linesegment";
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Vector
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a 2D vector
|
||||||
|
*/
|
||||||
|
export type Vector = [u: number, v: number] & {
|
||||||
|
_brand: "excalimath__vector";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triangles
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A triangle represented by 3 points
|
||||||
|
*/
|
||||||
|
export type Triangle<P extends GlobalPoint | LocalPoint> = [
|
||||||
|
a: P,
|
||||||
|
b: P,
|
||||||
|
c: P,
|
||||||
|
] & {
|
||||||
|
_brand: "excalimath__triangle";
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Polygon
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A polygon is a closed shape by connecting the given points
|
||||||
|
* rectangles and diamonds are modelled by polygons
|
||||||
|
*/
|
||||||
|
export type Polygon<Point extends GlobalPoint | LocalPoint> = Point[] & {
|
||||||
|
_brand: "excalimath_polygon";
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Curve
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cubic bezier curve with four control points
|
||||||
|
*/
|
||||||
|
export type Curve<Point extends GlobalPoint | LocalPoint> = [
|
||||||
|
Point,
|
||||||
|
Point,
|
||||||
|
Point,
|
||||||
|
Point,
|
||||||
|
] & {
|
||||||
|
_brand: "excalimath_curve";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PolarCoords = [
|
||||||
|
radius: number,
|
||||||
|
/** angle in radians */
|
||||||
|
angle: number,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
|
||||||
|
* corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right".
|
||||||
|
*/
|
||||||
|
export type SymmetricArc = {
|
||||||
|
radius: number;
|
||||||
|
startAngle: number;
|
||||||
|
endAngle: number;
|
||||||
|
};
|
17
packages/math/utils.ts
Normal file
17
packages/math/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export const PRECISION = 10e-5;
|
||||||
|
|
||||||
|
export function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function round(value: number, precision: number) {
|
||||||
|
const multiplier = Math.pow(10, precision);
|
||||||
|
|
||||||
|
return Math.round((value + Number.EPSILON) * multiplier) / multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const average = (a: number, b: number) => (a + b) / 2;
|
||||||
|
|
||||||
|
export const isFiniteNumber = (value: any): value is number => {
|
||||||
|
return typeof value === "number" && Number.isFinite(value);
|
||||||
|
};
|
12
packages/math/vector.test.ts
Normal file
12
packages/math/vector.test.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { isVector } from ".";
|
||||||
|
|
||||||
|
describe("Vector", () => {
|
||||||
|
test("isVector", () => {
|
||||||
|
expect(isVector([5, 5])).toBe(true);
|
||||||
|
expect(isVector([-5, -5])).toBe(true);
|
||||||
|
expect(isVector([5, 0.5])).toBe(true);
|
||||||
|
expect(isVector(null)).toBe(false);
|
||||||
|
expect(isVector(undefined)).toBe(false);
|
||||||
|
expect(isVector([5, NaN])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
141
packages/math/vector.ts
Normal file
141
packages/math/vector.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import type { GlobalPoint, LocalPoint, Vector } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a vector from the x and y coordiante elements.
|
||||||
|
*
|
||||||
|
* @param x The X aspect of the vector
|
||||||
|
* @param y T Y aspect of the vector
|
||||||
|
* @returns The constructed vector with X and Y as the coordinates
|
||||||
|
*/
|
||||||
|
export function vector(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
originX: number = 0,
|
||||||
|
originY: number = 0,
|
||||||
|
): Vector {
|
||||||
|
return [x - originX, y - originY] as Vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a point into a vector with the origin point.
|
||||||
|
*
|
||||||
|
* @param p The point to turn into a 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 {
|
||||||
|
return vector(p[0] - origin[0], p[1] - origin[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross product is a binary operation on two vectors in 2D space.
|
||||||
|
* It results in a vector that is perpendicular to both vectors.
|
||||||
|
*
|
||||||
|
* @param a One of the vectors to use for the directed area calculation
|
||||||
|
* @param b The other vector to use for the directed area calculation
|
||||||
|
* @returns The directed area value for the two vectos
|
||||||
|
*/
|
||||||
|
export function vectorCross(a: Vector, b: Vector): number {
|
||||||
|
return a[0] * b[1] - b[0] * a[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dot product is defined as the sum of the products of the
|
||||||
|
* two vectors.
|
||||||
|
*
|
||||||
|
* @param a One of the vectors for which the sum of products is calculated
|
||||||
|
* @param b The other vector for which the sum of products is calculated
|
||||||
|
* @returns The sum of products of the two vectors
|
||||||
|
*/
|
||||||
|
export function vectorDot(a: Vector, b: Vector) {
|
||||||
|
return a[0] * b[0] + a[1] * b[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the value has the shape of a Vector.
|
||||||
|
*
|
||||||
|
* @param v The value to test
|
||||||
|
* @returns TRUE if the value has the shape and components of a Vectors
|
||||||
|
*/
|
||||||
|
export function isVector(v: unknown): v is Vector {
|
||||||
|
return (
|
||||||
|
Array.isArray(v) &&
|
||||||
|
v.length === 2 &&
|
||||||
|
typeof v[0] === "number" &&
|
||||||
|
!isNaN(v[0]) &&
|
||||||
|
typeof v[1] === "number" &&
|
||||||
|
!isNaN(v[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add two vectors by adding their coordinates.
|
||||||
|
*
|
||||||
|
* @param a One of the vectors to add
|
||||||
|
* @param b The other vector to add
|
||||||
|
* @returns The sum vector of the two provided vectors
|
||||||
|
*/
|
||||||
|
export function vectorAdd(a: Readonly<Vector>, b: Readonly<Vector>): Vector {
|
||||||
|
return [a[0] + b[0], a[1] + b[1]] as Vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add two vectors by adding their coordinates.
|
||||||
|
*
|
||||||
|
* @param start One of the vectors to add
|
||||||
|
* @param end The other vector to add
|
||||||
|
* @returns The sum vector of the two provided vectors
|
||||||
|
*/
|
||||||
|
export function vectorSubtract(
|
||||||
|
start: Readonly<Vector>,
|
||||||
|
end: Readonly<Vector>,
|
||||||
|
): Vector {
|
||||||
|
return [start[0] - end[0], start[1] - end[1]] as Vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale vector by a scalar.
|
||||||
|
*
|
||||||
|
* @param v The vector to scale
|
||||||
|
* @param scalar The scalar to multiply the vector components with
|
||||||
|
* @returns The new scaled vector
|
||||||
|
*/
|
||||||
|
export function vectorScale(v: Vector, scalar: number): Vector {
|
||||||
|
return vector(v[0] * scalar, v[1] * scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the sqare magnitude of a vector. Use this if you compare
|
||||||
|
* magnitudes as it saves you an SQRT.
|
||||||
|
*
|
||||||
|
* @param v The vector to measure
|
||||||
|
* @returns The scalar squared magnitude of the vector
|
||||||
|
*/
|
||||||
|
export function vectorMagnitudeSq(v: Vector) {
|
||||||
|
return v[0] * v[0] + v[1] * v[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the magnitude of a vector.
|
||||||
|
*
|
||||||
|
* @param v The vector to measure
|
||||||
|
* @returns The scalar magnitude of the vector
|
||||||
|
*/
|
||||||
|
export function vectorMagnitude(v: Vector) {
|
||||||
|
return Math.sqrt(vectorMagnitudeSq(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the vector (i.e. make the vector magnitue equal 1).
|
||||||
|
*
|
||||||
|
* @param v The vector to normalize
|
||||||
|
* @returns The new normalized vector
|
||||||
|
*/
|
||||||
|
export const vectorNormalize = (v: Vector): Vector => {
|
||||||
|
const m = vectorMagnitude(v);
|
||||||
|
|
||||||
|
return vector(v[0] / m, v[1] / m);
|
||||||
|
};
|
55
packages/math/webpack.prod.config.js
Normal file
55
packages/math/webpack.prod.config.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const path = require("path");
|
||||||
|
const BundleAnalyzerPlugin =
|
||||||
|
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: "production",
|
||||||
|
entry: { "excalidraw-math.min": "./index.js" },
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
filename: "[name].js",
|
||||||
|
library: "ExcalidrawMath",
|
||||||
|
libraryTarget: "umd",
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".tsx", ".ts", ".js", ".css", ".scss"],
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
runtimeChunk: false,
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(ts|tsx|js)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
configFile: path.resolve(__dirname, "../tsconfig.prod.json"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: "babel-loader",
|
||||||
|
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
"@babel/preset-env",
|
||||||
|
["@babel/preset-react", { runtime: "automatic" }],
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
],
|
||||||
|
plugins: [["@babel/plugin-transform-runtime"]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []),
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,9 +1,16 @@
|
||||||
|
import {
|
||||||
|
vectorCross,
|
||||||
|
vectorFromPoint,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
} from "../math";
|
||||||
import type { Bounds } from "../excalidraw/element/bounds";
|
import type { Bounds } from "../excalidraw/element/bounds";
|
||||||
import type { Point } from "../excalidraw/types";
|
|
||||||
|
|
||||||
export type LineSegment = [Point, Point];
|
export type LineSegment<P extends LocalPoint | GlobalPoint> = [P, P];
|
||||||
|
|
||||||
export function getBBox(line: LineSegment): Bounds {
|
export function getBBox<P extends LocalPoint | GlobalPoint>(
|
||||||
|
line: LineSegment<P>,
|
||||||
|
): Bounds {
|
||||||
return [
|
return [
|
||||||
Math.min(line[0][0], line[1][0]),
|
Math.min(line[0][0], line[1][0]),
|
||||||
Math.min(line[0][1], line[1][1]),
|
Math.min(line[0][1], line[1][1]),
|
||||||
|
@ -12,40 +19,37 @@ export function getBBox(line: LineSegment): Bounds {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function crossProduct(a: Point, b: Point) {
|
|
||||||
return a[0] * b[1] - b[0] * a[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
|
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
|
||||||
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function translate(a: Point, b: Point): Point {
|
|
||||||
return [a[0] - b[0], a[1] - b[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EPSILON = 0.000001;
|
const EPSILON = 0.000001;
|
||||||
|
|
||||||
export function isPointOnLine(l: LineSegment, p: Point) {
|
export function isPointOnLine<P extends GlobalPoint | LocalPoint>(
|
||||||
const p1 = translate(l[1], l[0]);
|
l: LineSegment<P>,
|
||||||
const p2 = translate(p, l[0]);
|
p: P,
|
||||||
|
) {
|
||||||
|
const p1 = vectorFromPoint(l[1], l[0]);
|
||||||
|
const p2 = vectorFromPoint(p, l[0]);
|
||||||
|
|
||||||
const r = crossProduct(p1, p2);
|
const r = vectorCross(p1, p2);
|
||||||
|
|
||||||
return Math.abs(r) < EPSILON;
|
return Math.abs(r) < EPSILON;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPointRightOfLine(l: LineSegment, p: Point) {
|
export function isPointRightOfLine<P extends GlobalPoint | LocalPoint>(
|
||||||
const p1 = translate(l[1], l[0]);
|
l: LineSegment<P>,
|
||||||
const p2 = translate(p, l[0]);
|
p: P,
|
||||||
|
) {
|
||||||
|
const p1 = vectorFromPoint(l[1], l[0]);
|
||||||
|
const p2 = vectorFromPoint(p, l[0]);
|
||||||
|
|
||||||
return crossProduct(p1, p2) < 0;
|
return vectorCross(p1, p2) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLineSegmentTouchingOrCrossingLine(
|
export function isLineSegmentTouchingOrCrossingLine<
|
||||||
a: LineSegment,
|
P extends GlobalPoint | LocalPoint,
|
||||||
b: LineSegment,
|
>(a: LineSegment<P>, b: LineSegment<P>) {
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
isPointOnLine(a, b[0]) ||
|
isPointOnLine(a, b[0]) ||
|
||||||
isPointOnLine(a, b[1]) ||
|
isPointOnLine(a, b[1]) ||
|
||||||
|
@ -56,7 +60,10 @@ export function isLineSegmentTouchingOrCrossingLine(
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
||||||
export function doLineSegmentsIntersect(a: LineSegment, b: LineSegment) {
|
export function doLineSegmentsIntersect<P extends GlobalPoint | LocalPoint>(
|
||||||
|
a: LineSegment<P>,
|
||||||
|
b: LineSegment<P>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
|
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
|
||||||
isLineSegmentTouchingOrCrossingLine(a, b) &&
|
isLineSegmentTouchingOrCrossingLine(a, b) &&
|
||||||
|
|
87
packages/utils/collision.test.ts
Normal file
87
packages/utils/collision.test.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import type { Curve, Degrees, GlobalPoint } from "../math";
|
||||||
|
import {
|
||||||
|
curve,
|
||||||
|
degreesToRadians,
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentRotate,
|
||||||
|
point,
|
||||||
|
pointRotateDegs,
|
||||||
|
} from "../math";
|
||||||
|
import { pointOnCurve, pointOnPolyline } from "./collision";
|
||||||
|
import type { Polyline } from "./geometry/shape";
|
||||||
|
|
||||||
|
describe("point and curve", () => {
|
||||||
|
const c: Curve<GlobalPoint> = curve(
|
||||||
|
point(1.4, 1.65),
|
||||||
|
point(1.9, 7.9),
|
||||||
|
point(5.9, 1.65),
|
||||||
|
point(6.44, 4.84),
|
||||||
|
);
|
||||||
|
|
||||||
|
it("point on curve", () => {
|
||||||
|
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
||||||
|
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
||||||
|
|
||||||
|
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true);
|
||||||
|
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true);
|
||||||
|
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true);
|
||||||
|
|
||||||
|
expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false);
|
||||||
|
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
||||||
|
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("point and polylines", () => {
|
||||||
|
const polyline: Polyline<GlobalPoint> = [
|
||||||
|
lineSegment(point(1, 0), point(1, 2)),
|
||||||
|
lineSegment(point(1, 2), point(2, 2)),
|
||||||
|
lineSegment(point(2, 2), point(2, 1)),
|
||||||
|
lineSegment(point(2, 1), point(3, 1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
it("point on the line", () => {
|
||||||
|
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
|
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true);
|
||||||
|
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true);
|
||||||
|
|
||||||
|
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false);
|
||||||
|
expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("point on the line with rotation", () => {
|
||||||
|
const truePoints = [
|
||||||
|
point(1, 0),
|
||||||
|
point(1, 2),
|
||||||
|
point(2, 2),
|
||||||
|
point(2, 1),
|
||||||
|
point(3, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
truePoints.forEach((p) => {
|
||||||
|
const rotation = (Math.random() * 360) as Degrees;
|
||||||
|
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||||
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
|
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||||
|
);
|
||||||
|
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const falsePoints = [point(0, 1), point(2.1, 1.5)];
|
||||||
|
|
||||||
|
falsePoints.forEach((p) => {
|
||||||
|
const rotation = (Math.random() * 360) as Degrees;
|
||||||
|
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
|
||||||
|
const rotatedPolyline = polyline.map((line) =>
|
||||||
|
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)),
|
||||||
|
);
|
||||||
|
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,20 +1,26 @@
|
||||||
import type { Point, Polygon, GeometricShape } from "./geometry/shape";
|
import type { Polycurve, Polyline } from "./geometry/shape";
|
||||||
import {
|
import {
|
||||||
pointInEllipse,
|
pointInEllipse,
|
||||||
pointInPolygon,
|
|
||||||
pointOnCurve,
|
|
||||||
pointOnEllipse,
|
pointOnEllipse,
|
||||||
pointOnLine,
|
type GeometricShape,
|
||||||
pointOnPolycurve,
|
} from "./geometry/shape";
|
||||||
|
import type { Curve } from "../math";
|
||||||
|
import {
|
||||||
|
lineSegment,
|
||||||
|
point,
|
||||||
|
polygonIncludesPoint,
|
||||||
|
pointOnLineSegment,
|
||||||
pointOnPolygon,
|
pointOnPolygon,
|
||||||
pointOnPolyline,
|
polygonFromPoints,
|
||||||
close,
|
type GlobalPoint,
|
||||||
} from "./geometry/geometry";
|
type LocalPoint,
|
||||||
|
type Polygon,
|
||||||
|
} 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 = (
|
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
point: Point,
|
point: Point,
|
||||||
shape: GeometricShape,
|
shape: GeometricShape<Point>,
|
||||||
tolerance = 0,
|
tolerance = 0,
|
||||||
) => {
|
) => {
|
||||||
// get the distance from the given point to the given element
|
// get the distance from the given point to the given element
|
||||||
|
@ -25,7 +31,7 @@ export const isPointOnShape = (
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return pointOnEllipse(point, shape.data, tolerance);
|
return pointOnEllipse(point, shape.data, tolerance);
|
||||||
case "line":
|
case "line":
|
||||||
return pointOnLine(point, shape.data, tolerance);
|
return pointOnLineSegment(point, shape.data, tolerance);
|
||||||
case "polyline":
|
case "polyline":
|
||||||
return pointOnPolyline(point, shape.data, tolerance);
|
return pointOnPolyline(point, shape.data, tolerance);
|
||||||
case "curve":
|
case "curve":
|
||||||
|
@ -38,10 +44,13 @@ export const isPointOnShape = (
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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: Point, shape: GeometricShape) => {
|
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
point: Point,
|
||||||
|
shape: GeometricShape<Point>,
|
||||||
|
) => {
|
||||||
switch (shape.type) {
|
switch (shape.type) {
|
||||||
case "polygon":
|
case "polygon":
|
||||||
return pointInPolygon(point, shape.data);
|
return polygonIncludesPoint(point, shape.data);
|
||||||
case "line":
|
case "line":
|
||||||
return false;
|
return false;
|
||||||
case "curve":
|
case "curve":
|
||||||
|
@ -49,8 +58,8 @@ export const isPointInShape = (point: Point, shape: GeometricShape) => {
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return pointInEllipse(point, shape.data);
|
return pointInEllipse(point, shape.data);
|
||||||
case "polyline": {
|
case "polyline": {
|
||||||
const polygon = close(shape.data.flat()) as Polygon;
|
const polygon = polygonFromPoints(shape.data.flat());
|
||||||
return pointInPolygon(point, polygon);
|
return polygonIncludesPoint(point, polygon);
|
||||||
}
|
}
|
||||||
case "polycurve": {
|
case "polycurve": {
|
||||||
return false;
|
return false;
|
||||||
|
@ -61,6 +70,67 @@ export const isPointInShape = (point: Point, shape: GeometricShape) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// check if the given element is in the given bounds
|
// check if the given element is in the given bounds
|
||||||
export const isPointInBounds = (point: Point, bounds: Polygon) => {
|
export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
|
||||||
return pointInPolygon(point, bounds);
|
point: Point,
|
||||||
|
bounds: Polygon<Point>,
|
||||||
|
) => {
|
||||||
|
return polygonIncludesPoint(point, bounds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
polycurve: Polycurve<Point>,
|
||||||
|
tolerance: number,
|
||||||
|
) => {
|
||||||
|
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
curve: Curve<Point>,
|
||||||
|
) => {
|
||||||
|
const [p0, p1, p2, p3] = curve;
|
||||||
|
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||||
|
return (t: number, idx: number) =>
|
||||||
|
Math.pow(1 - t, 3) * p3[idx] +
|
||||||
|
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
||||||
|
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
||||||
|
p0[idx] * Math.pow(t, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
curve: Curve<Point>,
|
||||||
|
segments = 10,
|
||||||
|
): Polyline<Point> => {
|
||||||
|
const equation = cubicBezierEquation(curve);
|
||||||
|
let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
|
||||||
|
const lineSegments: Polyline<Point> = [];
|
||||||
|
let t = 0;
|
||||||
|
const increment = 1 / segments;
|
||||||
|
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
t += increment;
|
||||||
|
if (t <= 1) {
|
||||||
|
const nextPoint: Point = point(equation(t, 0), equation(t, 1));
|
||||||
|
lineSegments.push(lineSegment(startingPoint, nextPoint));
|
||||||
|
startingPoint = nextPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineSegments;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
curve: Curve<Point>,
|
||||||
|
threshold: number,
|
||||||
|
) => {
|
||||||
|
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
polyline: Polyline<Point>,
|
||||||
|
threshold = 10e-5,
|
||||||
|
) => {
|
||||||
|
return polyline.some((line) => pointOnLineSegment(point, line, threshold));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,249 +1,122 @@
|
||||||
|
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
lineIntersectsLine,
|
point,
|
||||||
lineRotate,
|
lineSegment,
|
||||||
pointInEllipse,
|
polygon,
|
||||||
pointInPolygon,
|
pointOnLineSegment,
|
||||||
pointLeftofLine,
|
|
||||||
pointOnCurve,
|
|
||||||
pointOnEllipse,
|
|
||||||
pointOnLine,
|
|
||||||
pointOnPolygon,
|
pointOnPolygon,
|
||||||
pointOnPolyline,
|
polygonIncludesPoint,
|
||||||
pointRightofLine,
|
segmentsIntersectAt,
|
||||||
pointRotate,
|
} from "../../math";
|
||||||
} from "./geometry";
|
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
|
||||||
import type { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
|
|
||||||
|
|
||||||
describe("point and line", () => {
|
describe("point and line", () => {
|
||||||
const line: Line = [
|
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
||||||
[1, 0],
|
|
||||||
[1, 2],
|
|
||||||
];
|
|
||||||
|
|
||||||
it("point on left or right of line", () => {
|
// it("point on left or right of line", () => {
|
||||||
expect(pointLeftofLine([0, 1], line)).toBe(true);
|
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
|
||||||
expect(pointLeftofLine([1, 1], line)).toBe(false);
|
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
|
||||||
expect(pointLeftofLine([2, 1], line)).toBe(false);
|
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
|
||||||
|
|
||||||
expect(pointRightofLine([0, 1], line)).toBe(false);
|
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
|
||||||
expect(pointRightofLine([1, 1], line)).toBe(false);
|
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
|
||||||
expect(pointRightofLine([2, 1], line)).toBe(true);
|
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||||
|
|
||||||
it("point on the line", () => {
|
it("point on the line", () => {
|
||||||
expect(pointOnLine([0, 1], line)).toBe(false);
|
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
|
||||||
expect(pointOnLine([1, 1], line, 0)).toBe(true);
|
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
|
||||||
expect(pointOnLine([2, 1], line)).toBe(false);
|
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("point and polylines", () => {
|
|
||||||
const polyline: Polyline = [
|
|
||||||
[
|
|
||||||
[1, 0],
|
|
||||||
[1, 2],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[1, 2],
|
|
||||||
[2, 2],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[2, 2],
|
|
||||||
[2, 1],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[2, 1],
|
|
||||||
[3, 1],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
it("point on the line", () => {
|
|
||||||
expect(pointOnPolyline([1, 0], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([1, 2], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([2, 2], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([2, 1], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([3, 1], polyline)).toBe(true);
|
|
||||||
|
|
||||||
expect(pointOnPolyline([1, 1], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
|
|
||||||
expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
|
|
||||||
|
|
||||||
expect(pointOnPolyline([0, 1], polyline)).toBe(false);
|
|
||||||
expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("point on the line with rotation", () => {
|
|
||||||
const truePoints = [
|
|
||||||
[1, 0],
|
|
||||||
[1, 2],
|
|
||||||
[2, 2],
|
|
||||||
[2, 1],
|
|
||||||
[3, 1],
|
|
||||||
] as Point[];
|
|
||||||
|
|
||||||
truePoints.forEach((point) => {
|
|
||||||
const rotation = Math.random() * 360;
|
|
||||||
const rotatedPoint = pointRotate(point, rotation);
|
|
||||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
|
||||||
lineRotate(line, rotation, [0, 0]),
|
|
||||||
);
|
|
||||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const falsePoints = [
|
|
||||||
[0, 1],
|
|
||||||
[2.1, 1.5],
|
|
||||||
] as Point[];
|
|
||||||
|
|
||||||
falsePoints.forEach((point) => {
|
|
||||||
const rotation = Math.random() * 360;
|
|
||||||
const rotatedPoint = pointRotate(point, rotation);
|
|
||||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
|
||||||
lineRotate(line, rotation, [0, 0]),
|
|
||||||
);
|
|
||||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and polygon", () => {
|
describe("point and polygon", () => {
|
||||||
const polygon: Polygon = [
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
[10, 10],
|
point(10, 10),
|
||||||
[50, 10],
|
point(50, 10),
|
||||||
[50, 50],
|
point(50, 50),
|
||||||
[10, 50],
|
point(10, 50),
|
||||||
];
|
);
|
||||||
|
|
||||||
it("point on polygon", () => {
|
it("point on polygon", () => {
|
||||||
expect(pointOnPolygon([30, 10], polygon)).toBe(true);
|
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
|
||||||
expect(pointOnPolygon([50, 30], polygon)).toBe(true);
|
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon([30, 50], polygon)).toBe(true);
|
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
|
||||||
expect(pointOnPolygon([10, 30], polygon)).toBe(true);
|
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
|
||||||
expect(pointOnPolygon([30, 30], polygon)).toBe(false);
|
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
|
||||||
expect(pointOnPolygon([30, 70], polygon)).toBe(false);
|
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point in polygon", () => {
|
it("point in polygon", () => {
|
||||||
const polygon: Polygon = [
|
const poly: Polygon<GlobalPoint> = polygon(
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[2, 0],
|
point(2, 0),
|
||||||
[2, 2],
|
point(2, 2),
|
||||||
[0, 2],
|
point(0, 2),
|
||||||
];
|
);
|
||||||
expect(pointInPolygon([1, 1], polygon)).toBe(true);
|
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
|
||||||
expect(pointInPolygon([3, 3], polygon)).toBe(false);
|
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("point and curve", () => {
|
|
||||||
const curve: Curve = [
|
|
||||||
[1.4, 1.65],
|
|
||||||
[1.9, 7.9],
|
|
||||||
[5.9, 1.65],
|
|
||||||
[6.44, 4.84],
|
|
||||||
];
|
|
||||||
|
|
||||||
it("point on curve", () => {
|
|
||||||
expect(pointOnCurve(curve[0], curve)).toBe(true);
|
|
||||||
expect(pointOnCurve(curve[3], curve)).toBe(true);
|
|
||||||
|
|
||||||
expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
|
|
||||||
expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
|
|
||||||
expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
|
|
||||||
|
|
||||||
expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
|
|
||||||
expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
|
|
||||||
expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and ellipse", () => {
|
describe("point and ellipse", () => {
|
||||||
const ellipse: Ellipse = {
|
const ellipse: Ellipse<GlobalPoint> = {
|
||||||
center: [0, 0],
|
center: point(0, 0),
|
||||||
angle: 0,
|
angle: 0 as Radians,
|
||||||
halfWidth: 2,
|
halfWidth: 2,
|
||||||
halfHeight: 1,
|
halfHeight: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("point on ellipse", () => {
|
it("point on ellipse", () => {
|
||||||
[
|
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||||
[0, 1],
|
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||||
[0, -1],
|
|
||||||
[2, 0],
|
|
||||||
[-2, 0],
|
|
||||||
].forEach((point) => {
|
|
||||||
expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
|
|
||||||
});
|
});
|
||||||
expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse([-1.4, 0.71], ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse([1.4, 0.7], ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse([1.4, 0.71], ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.1)).toBe(true);
|
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.01)).toBe(true);
|
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse([-1, 0.8], ellipse)).toBe(false);
|
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
|
||||||
expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
|
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("point in ellipse", () => {
|
it("point in ellipse", () => {
|
||||||
[
|
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||||
[0, 1],
|
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||||
[0, -1],
|
|
||||||
[2, 0],
|
|
||||||
[-2, 0],
|
|
||||||
].forEach((point) => {
|
|
||||||
expect(pointInEllipse(point as Point, ellipse)).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
|
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
|
||||||
expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
|
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
|
||||||
|
|
||||||
expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
|
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
||||||
expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
|
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("line and line", () => {
|
describe("line and line", () => {
|
||||||
const lineA: Line = [
|
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
||||||
[1, 4],
|
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
||||||
[3, 4],
|
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
];
|
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
const lineB: Line = [
|
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
||||||
[2, 1],
|
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
||||||
[2, 7],
|
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
||||||
];
|
|
||||||
const lineC: Line = [
|
|
||||||
[1, 8],
|
|
||||||
[3, 8],
|
|
||||||
];
|
|
||||||
const lineD: Line = [
|
|
||||||
[1, 8],
|
|
||||||
[3, 8],
|
|
||||||
];
|
|
||||||
const lineE: Line = [
|
|
||||||
[1, 9],
|
|
||||||
[3, 9],
|
|
||||||
];
|
|
||||||
const lineF: Line = [
|
|
||||||
[1, 2],
|
|
||||||
[3, 4],
|
|
||||||
];
|
|
||||||
const lineG: Line = [
|
|
||||||
[0, 1],
|
|
||||||
[2, 3],
|
|
||||||
];
|
|
||||||
|
|
||||||
it("intersection", () => {
|
it("intersection", () => {
|
||||||
expect(lineIntersectsLine(lineA, lineB)).toBe(true);
|
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
||||||
expect(lineIntersectsLine(lineA, lineC)).toBe(false);
|
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
|
||||||
expect(lineIntersectsLine(lineB, lineC)).toBe(false);
|
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
|
||||||
expect(lineIntersectsLine(lineC, lineD)).toBe(true);
|
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
|
||||||
expect(lineIntersectsLine(lineE, lineD)).toBe(false);
|
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
|
||||||
expect(lineIntersectsLine(lineF, lineG)).toBe(true);
|
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,9 +12,30 @@
|
||||||
* to pure shapes
|
* to pure shapes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Curve, LineSegment, Polygon, Radians } from "../../math";
|
||||||
|
import {
|
||||||
|
curve,
|
||||||
|
lineSegment,
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
pointFromArray,
|
||||||
|
pointFromVector,
|
||||||
|
pointRotateRads,
|
||||||
|
polygon,
|
||||||
|
polygonFromPoints,
|
||||||
|
PRECISION,
|
||||||
|
segmentsIntersectAt,
|
||||||
|
vector,
|
||||||
|
vectorAdd,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
} from "../../math";
|
||||||
import { getElementAbsoluteCoords } from "../../excalidraw/element";
|
import { getElementAbsoluteCoords } from "../../excalidraw/element";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
@ -28,67 +49,54 @@ import type {
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../../excalidraw/element/types";
|
} from "../../excalidraw/element/types";
|
||||||
import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
|
|
||||||
import { pointsOnBezierCurves } from "points-on-curve";
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
|
import { invariant } from "../../excalidraw/utils";
|
||||||
// a point is specified by its coordinate (x, y)
|
|
||||||
export type Point = [number, number];
|
|
||||||
export type Vector = Point;
|
|
||||||
|
|
||||||
// a line (segment) is defined by two endpoints
|
|
||||||
export type Line = [Point, Point];
|
|
||||||
|
|
||||||
// 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 = Line[];
|
export type Polyline<Point extends GlobalPoint | LocalPoint> =
|
||||||
|
LineSegment<Point>[];
|
||||||
// cubic bezier curve with four control points
|
|
||||||
export type Curve = [Point, Point, Point, 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 = Curve[];
|
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
|
||||||
|
|
||||||
// a polygon is a closed shape by connecting the given points
|
|
||||||
// rectangles and diamonds are modelled by polygons
|
|
||||||
export type Polygon = 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 = {
|
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||||
center: Point;
|
center: Point;
|
||||||
angle: number;
|
angle: Radians;
|
||||||
halfWidth: number;
|
halfWidth: number;
|
||||||
halfHeight: number;
|
halfHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GeometricShape =
|
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||||
| {
|
| {
|
||||||
type: "line";
|
type: "line";
|
||||||
data: Line;
|
data: LineSegment<Point>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "polygon";
|
type: "polygon";
|
||||||
data: Polygon;
|
data: Polygon<Point>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "curve";
|
type: "curve";
|
||||||
data: Curve;
|
data: Curve<Point>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "ellipse";
|
type: "ellipse";
|
||||||
data: Ellipse;
|
data: Ellipse<Point>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "polyline";
|
type: "polyline";
|
||||||
data: Polyline;
|
data: Polyline<Point>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "polycurve";
|
type: "polycurve";
|
||||||
data: Polycurve;
|
data: Polycurve<Point>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RectangularElement =
|
type RectangularElement =
|
||||||
|
@ -102,32 +110,32 @@ type RectangularElement =
|
||||||
| ExcalidrawSelectionElement;
|
| ExcalidrawSelectionElement;
|
||||||
|
|
||||||
// polygon
|
// polygon
|
||||||
export const getPolygonShape = (
|
export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: RectangularElement,
|
element: RectangularElement,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
const { angle, width, height, x, y } = element;
|
const { angle, width, height, x, y } = element;
|
||||||
const angleInDegrees = angleToDegrees(angle);
|
|
||||||
const cx = x + width / 2;
|
const cx = x + width / 2;
|
||||||
const cy = y + height / 2;
|
const cy = y + height / 2;
|
||||||
|
|
||||||
const center: Point = [cx, cy];
|
const center: Point = point(cx, cy);
|
||||||
|
|
||||||
let data: Polygon = [];
|
let data: Polygon<Point>;
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
data = [
|
data = polygon(
|
||||||
pointRotate([cx, y], angleInDegrees, center),
|
pointRotateRads(point(cx, y), center, angle),
|
||||||
pointRotate([x + width, cy], angleInDegrees, center),
|
pointRotateRads(point(x + width, cy), center, angle),
|
||||||
pointRotate([cx, y + height], angleInDegrees, center),
|
pointRotateRads(point(cx, y + height), center, angle),
|
||||||
pointRotate([x, cy], angleInDegrees, center),
|
pointRotateRads(point(x, cy), center, angle),
|
||||||
] as Polygon;
|
);
|
||||||
} else {
|
} else {
|
||||||
data = [
|
data = polygon(
|
||||||
pointRotate([x, y], angleInDegrees, center),
|
pointRotateRads(point(x, y), center, angle),
|
||||||
pointRotate([x + width, y], angleInDegrees, center),
|
pointRotateRads(point(x + width, y), center, angle),
|
||||||
pointRotate([x + width, y + height], angleInDegrees, center),
|
pointRotateRads(point(x + width, y + height), center, angle),
|
||||||
pointRotate([x, y + height], angleInDegrees, center),
|
pointRotateRads(point(x, y + height), center, angle),
|
||||||
] as Polygon;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -137,7 +145,7 @@ export const getPolygonShape = (
|
||||||
};
|
};
|
||||||
|
|
||||||
// return the selection box for an element, possibly rotated as well
|
// return the selection box for an element, possibly rotated as well
|
||||||
export const getSelectionBoxShape = (
|
export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
padding = 10,
|
padding = 10,
|
||||||
|
@ -153,29 +161,29 @@ export const getSelectionBoxShape = (
|
||||||
y1 -= padding;
|
y1 -= padding;
|
||||||
y2 += padding;
|
y2 += padding;
|
||||||
|
|
||||||
const angleInDegrees = angleToDegrees(element.angle);
|
//const angleInDegrees = angleToDegrees(element.angle);
|
||||||
const center: Point = [cx, cy];
|
const center = point(cx, cy);
|
||||||
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
|
const topLeft = pointRotateRads(point(x1, y1), center, element.angle);
|
||||||
const topRight = pointRotate([x2, y1], angleInDegrees, center);
|
const topRight = pointRotateRads(point(x2, y1), center, element.angle);
|
||||||
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
|
const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle);
|
||||||
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
|
const bottomRight = pointRotateRads(point(x2, y2), center, element.angle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
data: [topLeft, topRight, bottomRight, bottomLeft],
|
data: [topLeft, topRight, bottomRight, bottomLeft],
|
||||||
} as GeometricShape;
|
} as GeometricShape<Point>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ellipse
|
// ellipse
|
||||||
export const getEllipseShape = (
|
export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
const { width, height, angle, x, y } = element;
|
const { width, height, angle, x, y } = element;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
data: {
|
data: {
|
||||||
center: [x + width / 2, y + height / 2],
|
center: point(x + width / 2, y + height / 2),
|
||||||
angle,
|
angle,
|
||||||
halfWidth: width / 2,
|
halfWidth: width / 2,
|
||||||
halfHeight: height / 2,
|
halfHeight: height / 2,
|
||||||
|
@ -193,32 +201,34 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// linear
|
// linear
|
||||||
export const getCurveShape = (
|
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
startingPoint: Point = [0, 0],
|
startingPoint: Point = point(0, 0),
|
||||||
angleInRadian: number,
|
angleInRadian: Radians,
|
||||||
center: Point,
|
center: Point,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
const transform = (p: Point) =>
|
const transform = (p: Point): Point =>
|
||||||
pointRotate(
|
pointRotateRads(
|
||||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||||
angleToDegrees(angleInRadian),
|
|
||||||
center,
|
center,
|
||||||
|
angleInRadian,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ops = getCurvePathOps(roughShape);
|
const ops = getCurvePathOps(roughShape);
|
||||||
const polycurve: Polycurve = [];
|
const polycurve: Polycurve<Point> = [];
|
||||||
let p0: Point = [0, 0];
|
let p0 = point<Point>(0, 0);
|
||||||
|
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
if (op.op === "move") {
|
if (op.op === "move") {
|
||||||
p0 = transform(op.data as Point);
|
const p = pointFromArray<Point>(op.data);
|
||||||
|
invariant(p != null, "Ops data is not a point");
|
||||||
|
p0 = transform(p);
|
||||||
}
|
}
|
||||||
if (op.op === "bcurveTo") {
|
if (op.op === "bcurveTo") {
|
||||||
const p1: Point = transform([op.data[0], op.data[1]]);
|
const p1 = transform(point<Point>(op.data[0], op.data[1]));
|
||||||
const p2: Point = transform([op.data[2], op.data[3]]);
|
const p2 = transform(point<Point>(op.data[2], op.data[3]));
|
||||||
const p3: Point = transform([op.data[4], op.data[5]]);
|
const p3 = transform(point<Point>(op.data[4], op.data[5]));
|
||||||
polycurve.push([p0, p1, p2, p3]);
|
polycurve.push(curve<Point>(p0, p1, p2, p3));
|
||||||
p0 = p3;
|
p0 = p3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,61 +239,72 @@ export const getCurveShape = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const polylineFromPoints = (points: Point[]) => {
|
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||||
let previousPoint = points[0];
|
points: Point[],
|
||||||
const polyline: Polyline = [];
|
): Polyline<Point> => {
|
||||||
|
let previousPoint: Point = points[0];
|
||||||
|
const polyline: LineSegment<Point>[] = [];
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
const nextPoint = points[i];
|
const nextPoint = points[i];
|
||||||
polyline.push([previousPoint, nextPoint]);
|
polyline.push(lineSegment<Point>(previousPoint, nextPoint));
|
||||||
previousPoint = nextPoint;
|
previousPoint = nextPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return polyline;
|
return polyline;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFreedrawShape = (
|
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
center: Point,
|
center: Point,
|
||||||
isClosed: boolean = false,
|
isClosed: boolean = false,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
const angle = angleToDegrees(element.angle);
|
|
||||||
const transform = (p: Point) =>
|
const transform = (p: Point) =>
|
||||||
pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
|
pointRotateRads(
|
||||||
|
pointFromVector(
|
||||||
|
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
const polyline = polylineFromPoints(
|
const polyline = polylineFromPoints(
|
||||||
element.points.map((p) => transform(p as Point)),
|
element.points.map((p) => transform(p as Point)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isClosed
|
return (
|
||||||
? {
|
isClosed
|
||||||
type: "polygon",
|
? {
|
||||||
data: close(polyline.flat()) as Polygon,
|
type: "polygon",
|
||||||
}
|
data: polygonFromPoints(polyline.flat()),
|
||||||
: {
|
}
|
||||||
type: "polyline",
|
: {
|
||||||
data: polyline,
|
type: "polyline",
|
||||||
};
|
data: polyline,
|
||||||
|
}
|
||||||
|
) as GeometricShape<Point>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClosedCurveShape = (
|
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
startingPoint: Point = [0, 0],
|
startingPoint: Point = point<Point>(0, 0),
|
||||||
angleInRadian: number,
|
angleInRadian: Radians,
|
||||||
center: Point,
|
center: Point,
|
||||||
): GeometricShape => {
|
): GeometricShape<Point> => {
|
||||||
const transform = (p: Point) =>
|
const transform = (p: Point) =>
|
||||||
pointRotate(
|
pointRotateRads(
|
||||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||||
angleToDegrees(angleInRadian),
|
|
||||||
center,
|
center,
|
||||||
|
angleInRadian,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (element.roundness === null) {
|
if (element.roundness === null) {
|
||||||
return {
|
return {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
data: close(element.points.map((p) => transform(p as Point))),
|
data: polygonFromPoints(
|
||||||
|
element.points.map((p) => transform(p as Point)) as Point[],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,27 +316,218 @@ export const getClosedCurveShape = (
|
||||||
if (operation.op === "move") {
|
if (operation.op === "move") {
|
||||||
odd = !odd;
|
odd = !odd;
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push([operation.data[0], operation.data[1]]);
|
points.push(point(operation.data[0], operation.data[1]));
|
||||||
}
|
}
|
||||||
} else if (operation.op === "bcurveTo") {
|
} else if (operation.op === "bcurveTo") {
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push([operation.data[0], operation.data[1]]);
|
points.push(point(operation.data[0], operation.data[1]));
|
||||||
points.push([operation.data[2], operation.data[3]]);
|
points.push(point(operation.data[2], operation.data[3]));
|
||||||
points.push([operation.data[4], operation.data[5]]);
|
points.push(point(operation.data[4], operation.data[5]));
|
||||||
}
|
}
|
||||||
} else if (operation.op === "lineTo") {
|
} else if (operation.op === "lineTo") {
|
||||||
if (odd) {
|
if (odd) {
|
||||||
points.push([operation.data[0], operation.data[1]]);
|
points.push(point(operation.data[0], operation.data[1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
||||||
transform(p),
|
transform(p as Point),
|
||||||
);
|
) as Point[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "polygon",
|
type: "polygon",
|
||||||
data: polygonPoints,
|
data: polygonFromPoints<Point>(polygonPoints),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine intersection of a rectangular shaped element and a
|
||||||
|
* line segment.
|
||||||
|
*
|
||||||
|
* @param element The rectangular element to test against
|
||||||
|
* @param segment The segment intersecting the element
|
||||||
|
* @param gap Optional value to inflate the shape before testing
|
||||||
|
* @returns An array of intersections
|
||||||
|
*/
|
||||||
|
// TODO: Replace with final rounded rectangle code
|
||||||
|
export const segmentIntersectRectangleElement = <
|
||||||
|
Point extends LocalPoint | GlobalPoint,
|
||||||
|
>(
|
||||||
|
element: ExcalidrawBindableElement,
|
||||||
|
segment: LineSegment<Point>,
|
||||||
|
gap: number = 0,
|
||||||
|
): Point[] => {
|
||||||
|
const bounds = [
|
||||||
|
element.x - gap,
|
||||||
|
element.y - gap,
|
||||||
|
element.x + element.width + gap,
|
||||||
|
element.y + element.height + gap,
|
||||||
|
];
|
||||||
|
const center = point(
|
||||||
|
(bounds[0] + bounds[2]) / 2,
|
||||||
|
(bounds[1] + bounds[3]) / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
|
||||||
|
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
|
||||||
|
),
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
|
||||||
|
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
|
||||||
|
),
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
|
||||||
|
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
|
||||||
|
),
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
|
||||||
|
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((s) => segmentsIntersectAt(segment, s))
|
||||||
|
.filter((i): i is Point => !!i);
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
p: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { angle, halfWidth, halfHeight, center } = ellipse;
|
||||||
|
const a = halfWidth;
|
||||||
|
const b = halfHeight;
|
||||||
|
const translatedPoint = vectorAdd(
|
||||||
|
vectorFromPoint(p),
|
||||||
|
vectorScale(vectorFromPoint(center), -1),
|
||||||
|
);
|
||||||
|
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||||
|
pointFromVector(translatedPoint),
|
||||||
|
point(0, 0),
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
const px = Math.abs(rotatedPointX);
|
||||||
|
const py = Math.abs(rotatedPointY);
|
||||||
|
|
||||||
|
let tx = 0.707;
|
||||||
|
let ty = 0.707;
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const x = a * tx;
|
||||||
|
const y = b * ty;
|
||||||
|
|
||||||
|
const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||||
|
const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||||
|
|
||||||
|
const rx = x - ex;
|
||||||
|
const ry = y - ey;
|
||||||
|
|
||||||
|
const qx = px - ex;
|
||||||
|
const qy = py - ey;
|
||||||
|
|
||||||
|
const r = Math.hypot(ry, rx);
|
||||||
|
const q = Math.hypot(qy, qx);
|
||||||
|
|
||||||
|
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
|
||||||
|
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
|
||||||
|
const t = Math.hypot(ty, tx);
|
||||||
|
tx /= t;
|
||||||
|
ty /= t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY] = [
|
||||||
|
a * tx * Math.sign(rotatedPointX),
|
||||||
|
b * ty * Math.sign(rotatedPointY),
|
||||||
|
];
|
||||||
|
|
||||||
|
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
point: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
threshold = PRECISION,
|
||||||
|
) => {
|
||||||
|
return distanceToEllipse(point, ellipse) <= threshold;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
p: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { center, angle, halfWidth, halfHeight } = ellipse;
|
||||||
|
const translatedPoint = vectorAdd(
|
||||||
|
vectorFromPoint(p),
|
||||||
|
vectorScale(vectorFromPoint(center), -1),
|
||||||
|
);
|
||||||
|
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||||
|
pointFromVector(translatedPoint),
|
||||||
|
point(0, 0),
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
|
||||||
|
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
|
||||||
|
1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
|
||||||
|
|
||||||
|
const majorAxis = widthGreaterThanHeight
|
||||||
|
? ellipse.halfWidth * 2
|
||||||
|
: ellipse.halfHeight * 2;
|
||||||
|
const minorAxis = widthGreaterThanHeight
|
||||||
|
? ellipse.halfHeight * 2
|
||||||
|
: ellipse.halfWidth * 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
majorAxis,
|
||||||
|
minorAxis,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||||
|
|
||||||
|
return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { center, angle } = ellipse;
|
||||||
|
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||||
|
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
|
||||||
|
const sqSum = majorAxis ** 2 + minorAxis ** 2;
|
||||||
|
const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
|
||||||
|
|
||||||
|
const yMax = Math.sqrt((sqSum - sqDiff) / 2);
|
||||||
|
const xAtYMax =
|
||||||
|
(yMax * sqSum * sin * cos) /
|
||||||
|
(majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
|
||||||
|
|
||||||
|
const xMax = Math.sqrt((sqSum + sqDiff) / 2);
|
||||||
|
const yAtXMax =
|
||||||
|
(xMax * sqSum * sin * cos) /
|
||||||
|
(majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
|
||||||
|
const centerVector = vectorFromPoint(center);
|
||||||
|
|
||||||
|
return [
|
||||||
|
vectorAdd(vector(xAtYMax, yMax), centerVector),
|
||||||
|
vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector),
|
||||||
|
vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||||
|
vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
|
@ -11,16 +11,21 @@ import {
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "../excalidraw/element/typeChecks";
|
} from "../excalidraw/element/typeChecks";
|
||||||
import { isValueInRange, rotatePoint } from "../excalidraw/math";
|
|
||||||
import type { Point } from "../excalidraw/types";
|
|
||||||
import type { Bounds } from "../excalidraw/element/bounds";
|
import type { Bounds } from "../excalidraw/element/bounds";
|
||||||
import { getElementBounds } from "../excalidraw/element/bounds";
|
import { getElementBounds } from "../excalidraw/element/bounds";
|
||||||
import { arrayToMap } from "../excalidraw/utils";
|
import { arrayToMap } from "../excalidraw/utils";
|
||||||
|
import type { LocalPoint } from "../math";
|
||||||
|
import {
|
||||||
|
rangeIncludesValue,
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
rangeInclusive,
|
||||||
|
} from "../math";
|
||||||
|
|
||||||
type Element = NonDeletedExcalidrawElement;
|
type Element = NonDeletedExcalidrawElement;
|
||||||
type Elements = readonly NonDeletedExcalidrawElement[];
|
type Elements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
type Points = readonly Point[];
|
type Points = readonly LocalPoint[];
|
||||||
|
|
||||||
/** @returns vertices relative to element's top-left [0,0] position */
|
/** @returns vertices relative to element's top-left [0,0] position */
|
||||||
const getNonLinearElementRelativePoints = (
|
const getNonLinearElementRelativePoints = (
|
||||||
|
@ -28,20 +33,25 @@ const getNonLinearElementRelativePoints = (
|
||||||
Element,
|
Element,
|
||||||
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
|
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
|
||||||
>,
|
>,
|
||||||
): [TopLeft: Point, TopRight: Point, BottomRight: Point, BottomLeft: Point] => {
|
): [
|
||||||
|
TopLeft: LocalPoint,
|
||||||
|
TopRight: LocalPoint,
|
||||||
|
BottomRight: LocalPoint,
|
||||||
|
BottomLeft: LocalPoint,
|
||||||
|
] => {
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
return [
|
return [
|
||||||
[element.width / 2, 0],
|
point(element.width / 2, 0),
|
||||||
[element.width, element.height / 2],
|
point(element.width, element.height / 2),
|
||||||
[element.width / 2, element.height],
|
point(element.width / 2, element.height),
|
||||||
[0, element.height / 2],
|
point(0, element.height / 2),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
[0, 0],
|
point(0, 0),
|
||||||
[0 + element.width, 0],
|
point(0 + element.width, 0),
|
||||||
[0 + element.width, element.height],
|
point(0 + element.width, element.height),
|
||||||
[0, element.height],
|
point(0, element.height),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,10 +94,10 @@ const getRotatedBBox = (element: Element): Bounds => {
|
||||||
const points = getElementRelativePoints(element);
|
const points = getElementRelativePoints(element);
|
||||||
|
|
||||||
const { cx, cy } = getMinMaxPoints(points);
|
const { cx, cy } = getMinMaxPoints(points);
|
||||||
const centerPoint: Point = [cx, cy];
|
const centerPoint = point<LocalPoint>(cx, cy);
|
||||||
|
|
||||||
const rotatedPoints = points.map((point) =>
|
const rotatedPoints = points.map((p) =>
|
||||||
rotatePoint([point[0], point[1]], centerPoint, element.angle),
|
pointRotateRads(p, centerPoint, element.angle),
|
||||||
);
|
);
|
||||||
const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
|
const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
|
||||||
|
|
||||||
|
@ -135,10 +145,16 @@ export const elementPartiallyOverlapsWithOrContainsBBox = (
|
||||||
const elementBBox = getRotatedBBox(element);
|
const elementBBox = getRotatedBBox(element);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(isValueInRange(elementBBox[0], bbox[0], bbox[2]) ||
|
(rangeIncludesValue(elementBBox[0], rangeInclusive(bbox[0], bbox[2])) ||
|
||||||
isValueInRange(bbox[0], elementBBox[0], elementBBox[2])) &&
|
rangeIncludesValue(
|
||||||
(isValueInRange(elementBBox[1], bbox[1], bbox[3]) ||
|
bbox[0],
|
||||||
isValueInRange(bbox[1], elementBBox[1], elementBBox[3]))
|
rangeInclusive(elementBBox[0], elementBBox[2]),
|
||||||
|
)) &&
|
||||||
|
(rangeIncludesValue(elementBBox[1], rangeInclusive(bbox[1], bbox[3])) ||
|
||||||
|
rangeIncludesValue(
|
||||||
|
bbox[1],
|
||||||
|
rangeInclusive(elementBBox[1], elementBBox[3]),
|
||||||
|
))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
108
scripts/buildMath.js
Normal file
108
scripts/buildMath.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const { build } = require("esbuild");
|
||||||
|
|
||||||
|
const browserConfig = {
|
||||||
|
entryPoints: ["index.ts"],
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Will be used later for treeshaking
|
||||||
|
|
||||||
|
// function getFiles(dir, files = []) {
|
||||||
|
// const fileList = fs.readdirSync(dir);
|
||||||
|
// for (const file of fileList) {
|
||||||
|
// const name = `${dir}/${file}`;
|
||||||
|
// if (
|
||||||
|
// name.includes("node_modules") ||
|
||||||
|
// name.includes("config") ||
|
||||||
|
// name.includes("package.json") ||
|
||||||
|
// name.includes("main.js") ||
|
||||||
|
// name.includes("index-node.ts") ||
|
||||||
|
// name.endsWith(".d.ts") ||
|
||||||
|
// name.endsWith(".md")
|
||||||
|
// ) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (fs.statSync(name).isDirectory()) {
|
||||||
|
// getFiles(name, files);
|
||||||
|
// } else if (
|
||||||
|
// name.match(/\.(sa|sc|c)ss$/) ||
|
||||||
|
// name.match(/\.(woff|woff2|eot|ttf|otf)$/) ||
|
||||||
|
// name.match(/locales\/[^/]+\.json$/)
|
||||||
|
// ) {
|
||||||
|
// continue;
|
||||||
|
// } else {
|
||||||
|
// files.push(name);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return files;
|
||||||
|
// }
|
||||||
|
const createESMBrowserBuild = async () => {
|
||||||
|
// Development unminified build with source maps
|
||||||
|
const browserDev = await build({
|
||||||
|
...browserConfig,
|
||||||
|
outdir: "dist/browser/dev",
|
||||||
|
sourcemap: true,
|
||||||
|
metafile: true,
|
||||||
|
define: {
|
||||||
|
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
"meta-browser-dev.json",
|
||||||
|
JSON.stringify(browserDev.metafile),
|
||||||
|
);
|
||||||
|
|
||||||
|
// production minified build without sourcemaps
|
||||||
|
const browserProd = await build({
|
||||||
|
...browserConfig,
|
||||||
|
outdir: "dist/browser/prod",
|
||||||
|
minify: true,
|
||||||
|
metafile: true,
|
||||||
|
define: {
|
||||||
|
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
"meta-browser-prod.json",
|
||||||
|
JSON.stringify(browserProd.metafile),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawConfig = {
|
||||||
|
entryPoints: ["index.ts"],
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
packages: "external",
|
||||||
|
};
|
||||||
|
|
||||||
|
const createESMRawBuild = async () => {
|
||||||
|
// Development unminified build with source maps
|
||||||
|
const rawDev = await build({
|
||||||
|
...rawConfig,
|
||||||
|
outdir: "dist/dev",
|
||||||
|
sourcemap: true,
|
||||||
|
metafile: true,
|
||||||
|
define: {
|
||||||
|
"import.meta.env": JSON.stringify({ DEV: true }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.writeFileSync("meta-raw-dev.json", JSON.stringify(rawDev.metafile));
|
||||||
|
|
||||||
|
// production minified build without sourcemaps
|
||||||
|
const rawProd = await build({
|
||||||
|
...rawConfig,
|
||||||
|
outdir: "dist/prod",
|
||||||
|
minify: true,
|
||||||
|
metafile: true,
|
||||||
|
define: {
|
||||||
|
"import.meta.env": JSON.stringify({ PROD: true }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.writeFileSync("meta-raw-prod.json", JSON.stringify(rawProd.metafile));
|
||||||
|
};
|
||||||
|
|
||||||
|
createESMRawBuild();
|
||||||
|
createESMBrowserBuild();
|
Loading…
Add table
Add a link
Reference in a new issue