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