merge with master

This commit is contained in:
Ryan Di 2025-04-14 16:18:11 +10:00
commit 39f79927ae
938 changed files with 127961 additions and 41753 deletions

View file

@ -1,65 +0,0 @@
import { Bounds } from "../excalidraw/element/bounds";
import { Point } from "../excalidraw/types";
export type LineSegment = [Point, Point];
export function getBBox(line: LineSegment): Bounds {
return [
Math.min(line[0][0], line[1][0]),
Math.min(line[0][1], line[1][1]),
Math.max(line[0][0], line[1][0]),
Math.max(line[0][1], line[1][1]),
];
}
export function crossProduct(a: Point, b: Point) {
return a[0] * b[1] - b[0] * a[1];
}
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
}
export function translate(a: Point, b: Point): Point {
return [a[0] - b[0], a[1] - b[1]];
}
const EPSILON = 0.000001;
export function isPointOnLine(l: LineSegment, p: Point) {
const p1 = translate(l[1], l[0]);
const p2 = translate(p, l[0]);
const r = crossProduct(p1, p2);
return Math.abs(r) < EPSILON;
}
export function isPointRightOfLine(l: LineSegment, p: Point) {
const p1 = translate(l[1], l[0]);
const p2 = translate(p, l[0]);
return crossProduct(p1, p2) < 0;
}
export function isLineSegmentTouchingOrCrossingLine(
a: LineSegment,
b: LineSegment,
) {
return (
isPointOnLine(a, b[0]) ||
isPointOnLine(a, b[1]) ||
(isPointRightOfLine(a, b[0])
? !isPointRightOfLine(a, b[1])
: isPointRightOfLine(a, b[1]))
);
}
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
export function doLineSegmentsIntersect(a: LineSegment, b: LineSegment) {
return (
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
isLineSegmentTouchingOrCrossingLine(a, b) &&
isLineSegmentTouchingOrCrossingLine(b, a)
);
}

3
packages/utils/global.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
import "@excalidraw/excalidraw/global";
import "@excalidraw/excalidraw/css";

View file

@ -1 +0,0 @@
export * from "./export";

View file

@ -1,11 +1,25 @@
{
"name": "@excalidraw/utils",
"version": "0.1.2",
"main": "dist/excalidraw-utils.min.js",
"type": "module",
"types": "./dist/types/utils/src/index.d.ts",
"main": "./dist/prod/index.js",
"module": "./dist/prod/index.js",
"exports": {
".": {
"types": "./dist/types/utils/src/index.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
},
"./*": {
"types": "./../utils/dist/types/utils/src/*.d.ts"
}
},
"files": [
"dist/*"
],
"description": "Excalidraw utilities functions",
"description": "Excalidraw utility functions",
"publishConfig": {
"access": "public"
},
@ -33,30 +47,29 @@
"last 1 safari version"
]
},
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/laser-pointer": "1.3.1",
"browser-fs-access": "0.29.1",
"open-color": "1.9.1",
"pako": "2.0.3",
"perfect-freehand": "1.2.0",
"png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0",
"roughjs": "4.6.4"
},
"devDependencies": {
"@babel/core": "7.18.9",
"@babel/plugin-transform-arrow-functions": "7.18.6",
"@babel/plugin-transform-async-to-generator": "7.18.6",
"@babel/plugin-transform-runtime": "7.18.6",
"@babel/plugin-transform-typescript": "7.18.8",
"@babel/preset-env": "7.18.9",
"@babel/preset-typescript": "7.18.6",
"babel-loader": "8.2.5",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "6.7.1",
"file-loader": "6.2.0",
"sass-loader": "13.0.2",
"ts-loader": "9.3.1",
"webpack": "5.76.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0"
"fonteditor-core": "2.4.0",
"typescript": "4.9.4",
"wawoff2": "2.0.1",
"which": "4.0.0"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw",
"scripts": {
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack"
"gen:types": "rm -rf types && tsc",
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types"
}
}

View file

@ -0,0 +1,73 @@
import {
vectorCross,
vectorFromPoint,
type GlobalPoint,
type LocalPoint,
} from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element/bounds";
export type LineSegment<P extends LocalPoint | GlobalPoint> = [P, P];
export function getBBox<P extends LocalPoint | GlobalPoint>(
line: LineSegment<P>,
): Bounds {
return [
Math.min(line[0][0], line[1][0]),
Math.min(line[0][1], line[1][1]),
Math.max(line[0][0], line[1][0]),
Math.max(line[0][1], line[1][1]),
];
}
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
}
const EPSILON = 0.000001;
export function isPointOnLine<P extends GlobalPoint | LocalPoint>(
l: LineSegment<P>,
p: P,
) {
const p1 = vectorFromPoint(l[1], l[0]);
const p2 = vectorFromPoint(p, l[0]);
const r = vectorCross(p1, p2);
return Math.abs(r) < EPSILON;
}
export function isPointRightOfLine<P extends GlobalPoint | LocalPoint>(
l: LineSegment<P>,
p: P,
) {
const p1 = vectorFromPoint(l[1], l[0]);
const p2 = vectorFromPoint(p, l[0]);
return vectorCross(p1, p2) < 0;
}
export function isLineSegmentTouchingOrCrossingLine<
P extends GlobalPoint | LocalPoint,
>(a: LineSegment<P>, b: LineSegment<P>) {
return (
isPointOnLine(a, b[0]) ||
isPointOnLine(a, b[1]) ||
(isPointRightOfLine(a, b[0])
? !isPointRightOfLine(a, b[1])
: isPointRightOfLine(a, b[1]))
);
}
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
export function doLineSegmentsIntersect<P extends GlobalPoint | LocalPoint>(
a: LineSegment<P>,
b: LineSegment<P>,
) {
return (
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
isLineSegmentTouchingOrCrossingLine(a, b) &&
isLineSegmentTouchingOrCrossingLine(b, a)
);
}

View file

@ -0,0 +1,135 @@
import {
lineSegment,
pointFrom,
polygonIncludesPoint,
pointOnLineSegment,
pointOnPolygon,
polygonFromPoints,
type GlobalPoint,
type LocalPoint,
type Polygon,
} from "@excalidraw/math";
import type { Curve } from "@excalidraw/math";
import { pointInEllipse, pointOnEllipse } from "./shape";
import type { Polycurve, Polyline, GeometricShape } from "./shape";
// check if the given point is considered on the given shape's border
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
point: Point,
shape: GeometricShape<Point>,
tolerance = 0,
) => {
// get the distance from the given point to the given element
// check if the distance is within the given epsilon range
switch (shape.type) {
case "polygon":
return pointOnPolygon(point, shape.data, tolerance);
case "ellipse":
return pointOnEllipse(point, shape.data, tolerance);
case "line":
return pointOnLineSegment(point, shape.data, tolerance);
case "polyline":
return pointOnPolyline(point, shape.data, tolerance);
case "curve":
return pointOnCurve(point, shape.data, tolerance);
case "polycurve":
return pointOnPolycurve(point, shape.data, tolerance);
default:
throw Error(`shape ${shape} is not implemented`);
}
};
// check if the given point is considered inside the element's border
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
point: Point,
shape: GeometricShape<Point>,
) => {
switch (shape.type) {
case "polygon":
return polygonIncludesPoint(point, shape.data);
case "line":
return false;
case "curve":
return false;
case "ellipse":
return pointInEllipse(point, shape.data);
case "polyline": {
const polygon = polygonFromPoints(shape.data.flat());
return polygonIncludesPoint(point, polygon);
}
case "polycurve": {
return false;
}
default:
throw Error(`shape ${shape} is not implemented`);
}
};
// check if the given element is in the given bounds
export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
point: Point,
bounds: Polygon<Point>,
) => {
return polygonIncludesPoint(point, bounds);
};
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
point: Point,
polycurve: Polycurve<Point>,
tolerance: number,
) => {
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
};
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
curve: Curve<Point>,
) => {
const [p0, p1, p2, p3] = curve;
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
return (t: number, idx: number) =>
Math.pow(1 - t, 3) * p3[idx] +
3 * t * Math.pow(1 - t, 2) * p2[idx] +
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
p0[idx] * Math.pow(t, 3);
};
const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
curve: Curve<Point>,
segments = 10,
): Polyline<Point> => {
const equation = cubicBezierEquation(curve);
let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
const lineSegments: Polyline<Point> = [];
let t = 0;
const increment = 1 / segments;
for (let i = 0; i < segments; i++) {
t += increment;
if (t <= 1) {
const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1));
lineSegments.push(lineSegment(startingPoint, nextPoint));
startingPoint = nextPoint;
}
}
return lineSegments;
};
export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
point: Point,
curve: Curve<Point>,
threshold: number,
) => {
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
};
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
point: Point,
polyline: Polyline<Point>,
threshold = 10e-5,
) => {
return polyline.some((line) => pointOnLineSegment(point, line, threshold));
};

View file

@ -1,23 +1,24 @@
import {
exportToCanvas as _exportToCanvas,
exportToSvg as _exportToSvg,
} from "../excalidraw/scene/export";
import { getDefaultAppState } from "../excalidraw/appState";
import { AppState, BinaryFiles } from "../excalidraw/types";
import {
ExcalidrawElement,
ExcalidrawFrameLikeElement,
NonDeleted,
} from "../excalidraw/element/types";
import { restore } from "../excalidraw/data/restore";
import { MIME_TYPES } from "../excalidraw/constants";
import { encodePngMetadata } from "../excalidraw/data/image";
import { serializeAsJSON } from "../excalidraw/data/json";
import { MIME_TYPES } from "@excalidraw/common";
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
import {
copyBlobToClipboardAsPng,
copyTextToSystemClipboard,
copyToClipboard,
} from "../excalidraw/clipboard";
} from "@excalidraw/excalidraw/clipboard";
import { encodePngMetadata } from "@excalidraw/excalidraw/data/image";
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
import { restore } from "@excalidraw/excalidraw/data/restore";
import {
exportToCanvas as _exportToCanvas,
exportToSvg as _exportToSvg,
} from "@excalidraw/excalidraw/scene/export";
import type {
ExcalidrawElement,
ExcalidrawFrameLikeElement,
NonDeleted,
} from "@excalidraw/element/types";
import type { AppState, BinaryFiles } from "@excalidraw/excalidraw/types";
export { MIME_TYPES };
@ -173,9 +174,13 @@ export const exportToSvg = async ({
exportPadding,
renderEmbeddables,
exportingFrame,
skipInliningFonts,
reuseImages,
}: Omit<ExportOpts, "getDimensions"> & {
exportPadding?: number;
renderEmbeddables?: boolean;
skipInliningFonts?: true;
reuseImages?: boolean;
}): Promise<SVGSVGElement> => {
const { elements: restoredElements, appState: restoredAppState } = restore(
{ elements, appState },
@ -191,6 +196,8 @@ export const exportToSvg = async ({
return _exportToSvg(restoredElements, exportAppState, files, {
exportingFrame,
renderEmbeddables,
skipInliningFonts,
reuseImages,
});
};
@ -212,21 +219,3 @@ export const exportToClipboard = async (
throw new Error("Invalid export type");
}
};
export * from "./bbox";
export {
elementsOverlappingBBox,
isElementInsideBBox,
elementPartiallyOverlapsWithOrContainsBBox,
} from "./withinBounds";
export {
serializeAsJSON,
serializeLibraryAsJSON,
} from "../excalidraw/data/json";
export {
loadFromBlob,
loadSceneOrLibraryFromBlob,
loadLibraryFromBlob,
} from "../excalidraw/data/blob";
export { getFreeDrawSvgPath } from "../excalidraw/renderer/renderElement";
export { mergeLibraryItems } from "../excalidraw/data/library";

View file

@ -0,0 +1,4 @@
export * from "./export";
export * from "./withinBounds";
export * from "./bbox";
export { getCommonBounds } from "@excalidraw/element/bounds";

544
packages/utils/src/shape.ts Normal file
View file

@ -0,0 +1,544 @@
/**
* this file defines pure geometric shapes
*
* for instance, a cubic bezier curve is specified by its four control points and
* an ellipse is defined by its center, angle, semi major axis and semi minor axis
* (but in semi-width and semi-height so it's more relevant to Excalidraw)
*
* the idea with pure shapes is so that we can provide collision and other geoemtric methods not depending on
* the specifics of roughjs or elements in Excalidraw; instead, we can focus on the pure shapes themselves
*
* also included in this file are methods for converting an Excalidraw element or a Drawable from roughjs
* to pure shapes
*/
import { pointsOnBezierCurves } from "points-on-curve";
import { invariant } from "@excalidraw/common";
import {
curve,
lineSegment,
pointFrom,
pointDistance,
pointFromArray,
pointFromVector,
pointRotateRads,
polygon,
polygonFromPoints,
PRECISION,
segmentsIntersectAt,
vector,
vectorAdd,
vectorFromPoint,
vectorScale,
type GlobalPoint,
type LocalPoint,
} from "@excalidraw/math";
import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
import type {
ElementsMap,
ExcalidrawBindableElement,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawEmbeddableElement,
ExcalidrawFrameLikeElement,
ExcalidrawFreeDrawElement,
ExcalidrawIframeElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawRectangleElement,
ExcalidrawSelectionElement,
ExcalidrawTextElement,
} from "@excalidraw/element/types";
import type { Curve, LineSegment, Polygon, Radians } from "@excalidraw/math";
import type { Drawable, Op } from "roughjs/bin/core";
// a polyline (made up term here) is a line consisting of other line segments
// this corresponds to a straight line element in the editor but it could also
// be used to model other elements
export type Polyline<Point extends GlobalPoint | LocalPoint> =
LineSegment<Point>[];
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
// curve on the canvas
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
// an ellipse is specified by its center, angle, and its major and minor axes
// but for the sake of simplicity, we've used halfWidth and halfHeight instead
// in replace of semi major and semi minor axes
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
center: Point;
angle: Radians;
halfWidth: number;
halfHeight: number;
};
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
| {
type: "line";
data: LineSegment<Point>;
}
| {
type: "polygon";
data: Polygon<Point>;
}
| {
type: "curve";
data: Curve<Point>;
}
| {
type: "ellipse";
data: Ellipse<Point>;
}
| {
type: "polyline";
data: Polyline<Point>;
}
| {
type: "polycurve";
data: Polycurve<Point>;
};
type RectangularElement =
| ExcalidrawRectangleElement
| ExcalidrawDiamondElement
| ExcalidrawFrameLikeElement
| ExcalidrawEmbeddableElement
| ExcalidrawImageElement
| ExcalidrawIframeElement
| ExcalidrawTextElement
| ExcalidrawSelectionElement;
// polygon
export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
element: RectangularElement,
): GeometricShape<Point> => {
const { angle, width, height, x, y } = element;
const cx = x + width / 2;
const cy = y + height / 2;
const center: Point = pointFrom(cx, cy);
let data: Polygon<Point>;
if (element.type === "diamond") {
data = polygon(
pointRotateRads(pointFrom(cx, y), center, angle),
pointRotateRads(pointFrom(x + width, cy), center, angle),
pointRotateRads(pointFrom(cx, y + height), center, angle),
pointRotateRads(pointFrom(x, cy), center, angle),
);
} else {
data = polygon(
pointRotateRads(pointFrom(x, y), center, angle),
pointRotateRads(pointFrom(x + width, y), center, angle),
pointRotateRads(pointFrom(x + width, y + height), center, angle),
pointRotateRads(pointFrom(x, y + height), center, angle),
);
}
return {
type: "polygon",
data,
};
};
// return the selection box for an element, possibly rotated as well
export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawElement,
elementsMap: ElementsMap,
padding = 10,
) => {
let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
true,
);
x1 -= padding;
x2 += padding;
y1 -= padding;
y2 += padding;
//const angleInDegrees = angleToDegrees(element.angle);
const center = pointFrom(cx, cy);
const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle);
const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle);
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle);
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle);
return {
type: "polygon",
data: [topLeft, topRight, bottomRight, bottomLeft],
} as GeometricShape<Point>;
};
// ellipse
export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawEllipseElement,
): GeometricShape<Point> => {
const { width, height, angle, x, y } = element;
return {
type: "ellipse",
data: {
center: pointFrom(x + width / 2, y + height / 2),
angle,
halfWidth: width / 2,
halfHeight: height / 2,
},
};
};
export const getCurvePathOps = (shape: Drawable): Op[] => {
// NOTE (mtolmacs): Temporary fix for extremely large elements
if (!shape) {
return [];
}
for (const set of shape.sets) {
if (set.type === "path") {
return set.ops;
}
}
return shape.sets[0].ops;
};
// linear
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
roughShape: Drawable,
startingPoint: Point = pointFrom(0, 0),
angleInRadian: Radians,
center: Point,
): GeometricShape<Point> => {
const transform = (p: Point): Point =>
pointRotateRads(
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center,
angleInRadian,
);
const ops = getCurvePathOps(roughShape);
const polycurve: Polycurve<Point> = [];
let p0 = pointFrom<Point>(0, 0);
for (const op of ops) {
if (op.op === "move") {
const p = pointFromArray<Point>(op.data);
invariant(p != null, "Ops data is not a point");
p0 = transform(p);
}
if (op.op === "bcurveTo") {
const p1 = transform(pointFrom<Point>(op.data[0], op.data[1]));
const p2 = transform(pointFrom<Point>(op.data[2], op.data[3]));
const p3 = transform(pointFrom<Point>(op.data[4], op.data[5]));
polycurve.push(curve<Point>(p0, p1, p2, p3));
p0 = p3;
}
}
return {
type: "polycurve",
data: polycurve,
};
};
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
points: Point[],
): Polyline<Point> => {
let previousPoint: Point = points[0];
const polyline: LineSegment<Point>[] = [];
for (let i = 1; i < points.length; i++) {
const nextPoint = points[i];
polyline.push(lineSegment<Point>(previousPoint, nextPoint));
previousPoint = nextPoint;
}
return polyline;
};
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawFreeDrawElement,
center: Point,
isClosed: boolean = false,
): GeometricShape<Point> => {
const transform = (p: Point) =>
pointRotateRads(
pointFromVector(
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
),
center,
element.angle,
);
const polyline = polylineFromPoints(
element.points.map((p) => transform(p as Point)),
);
return (
isClosed
? {
type: "polygon",
data: polygonFromPoints(polyline.flat()),
}
: {
type: "polyline",
data: polyline,
}
) as GeometricShape<Point>;
};
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawLinearElement,
roughShape: Drawable,
startingPoint: Point = pointFrom<Point>(0, 0),
angleInRadian: Radians,
center: Point,
): GeometricShape<Point> => {
const transform = (p: Point) =>
pointRotateRads(
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center,
angleInRadian,
);
if (element.roundness === null) {
return {
type: "polygon",
data: polygonFromPoints(
element.points.map((p) => transform(p as Point)) as Point[],
),
};
}
const ops = getCurvePathOps(roughShape);
const points: Point[] = [];
let odd = false;
for (const operation of ops) {
if (operation.op === "move") {
odd = !odd;
if (odd) {
points.push(pointFrom(operation.data[0], operation.data[1]));
}
} else if (operation.op === "bcurveTo") {
if (odd) {
points.push(pointFrom(operation.data[0], operation.data[1]));
points.push(pointFrom(operation.data[2], operation.data[3]));
points.push(pointFrom(operation.data[4], operation.data[5]));
}
} else if (operation.op === "lineTo") {
if (odd) {
points.push(pointFrom(operation.data[0], operation.data[1]));
}
}
}
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
transform(p as Point),
) as Point[];
return {
type: "polygon",
data: polygonFromPoints<Point>(polygonPoints),
};
};
/**
* Determine intersection of a rectangular shaped element and a
* line segment.
*
* @param element The rectangular element to test against
* @param segment The segment intersecting the element
* @param gap Optional value to inflate the shape before testing
* @returns An array of intersections
*/
// TODO: Replace with final rounded rectangle code
export const segmentIntersectRectangleElement = <
Point extends LocalPoint | GlobalPoint,
>(
element: ExcalidrawBindableElement,
segment: LineSegment<Point>,
gap: number = 0,
): Point[] => {
const bounds = [
element.x - gap,
element.y - gap,
element.x + element.width + gap,
element.y + element.height + gap,
];
const center = pointFrom(
(bounds[0] + bounds[2]) / 2,
(bounds[1] + bounds[3]) / 2,
);
return [
lineSegment(
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
),
lineSegment(
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
),
lineSegment(
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
),
lineSegment(
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
),
]
.map((s) => segmentsIntersectAt(segment, s))
.filter((i): i is Point => !!i);
};
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
p: Point,
ellipse: Ellipse<Point>,
) => {
const { angle, halfWidth, halfHeight, center } = ellipse;
const a = halfWidth;
const b = halfHeight;
const translatedPoint = vectorAdd(
vectorFromPoint(p),
vectorScale(vectorFromPoint(center), -1),
);
const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint),
pointFrom(0, 0),
-angle as Radians,
);
const px = Math.abs(rotatedPointX);
const py = Math.abs(rotatedPointY);
let tx = 0.707;
let ty = 0.707;
for (let i = 0; i < 3; i++) {
const x = a * tx;
const y = b * ty;
const ex = ((a * a - b * b) * tx ** 3) / a;
const ey = ((b * b - a * a) * ty ** 3) / b;
const rx = x - ex;
const ry = y - ey;
const qx = px - ex;
const qy = py - ey;
const r = Math.hypot(ry, rx);
const q = Math.hypot(qy, qx);
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
const t = Math.hypot(ty, tx);
tx /= t;
ty /= t;
}
const [minX, minY] = [
a * tx * Math.sign(rotatedPointX),
b * ty * Math.sign(rotatedPointY),
];
return pointDistance(
pointFrom(rotatedPointX, rotatedPointY),
pointFrom(minX, minY),
);
};
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
point: Point,
ellipse: Ellipse<Point>,
threshold = PRECISION,
) => {
return distanceToEllipse(point, ellipse) <= threshold;
};
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
p: Point,
ellipse: Ellipse<Point>,
) => {
const { center, angle, halfWidth, halfHeight } = ellipse;
const translatedPoint = vectorAdd(
vectorFromPoint(p),
vectorScale(vectorFromPoint(center), -1),
);
const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint),
pointFrom(0, 0),
-angle as Radians,
);
return (
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
1
);
};
export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
const majorAxis = widthGreaterThanHeight
? ellipse.halfWidth * 2
: ellipse.halfHeight * 2;
const minorAxis = widthGreaterThanHeight
? ellipse.halfHeight * 2
: ellipse.halfWidth * 2;
return {
majorAxis,
minorAxis,
};
};
export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
};
export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
const { center, angle } = ellipse;
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const sqSum = majorAxis ** 2 + minorAxis ** 2;
const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
const yMax = Math.sqrt((sqSum - sqDiff) / 2);
const xAtYMax =
(yMax * sqSum * sin * cos) /
(majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
const xMax = Math.sqrt((sqSum + sqDiff) / 2);
const yAtXMax =
(xMax * sqSum * sin * cos) /
(majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
const centerVector = vectorFromPoint(center);
return [
vectorAdd(vector(xAtYMax, yMax), centerVector),
vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector),
vectorAdd(vector(xMax, yAtXMax), centerVector),
vectorAdd(vector(xMax, yAtXMax), centerVector),
];
};

View file

@ -0,0 +1,33 @@
import { diffStringsUnified } from "jest-diff";
expect.extend({
toCloselyEqualPoints(received, expected, precision) {
if (!Array.isArray(received) || !Array.isArray(expected)) {
throw new Error("expected and received are not point arrays");
}
const COMPARE = 1 / Math.pow(10, precision || 2);
const pass = expected.every(
(point, idx) =>
Math.abs(received[idx]?.[0] - point[0]) < COMPARE &&
Math.abs(received[idx]?.[1] - point[1]) < COMPARE,
);
if (!pass) {
return {
message: () => ` The provided array of points are not close enough.
${diffStringsUnified(
JSON.stringify(expected, undefined, 2),
JSON.stringify(received, undefined, 2),
)}`,
pass: false,
};
}
return {
message: () => `expected ${received} to not be close to ${expected}`,
pass: true,
};
},
});

View file

@ -1,24 +1,32 @@
import type {
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
NonDeletedExcalidrawElement,
} from "../excalidraw/element/types";
import { arrayToMap } from "@excalidraw/common";
import { getElementBounds } from "@excalidraw/element/bounds";
import {
isArrowElement,
isExcalidrawElement,
isFreeDrawElement,
isLinearElement,
isTextElement,
} from "../excalidraw/element/typeChecks";
import { isValueInRange, rotatePoint } from "../excalidraw/math";
import type { Point } from "../excalidraw/types";
import { Bounds, getElementBounds } from "../excalidraw/element/bounds";
} from "@excalidraw/element/typeChecks";
import {
rangeIncludesValue,
pointFrom,
pointRotateRads,
rangeInclusive,
} from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element/bounds";
import type {
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
NonDeletedExcalidrawElement,
} from "@excalidraw/element/types";
import type { LocalPoint } from "@excalidraw/math";
type Element = NonDeletedExcalidrawElement;
type Elements = readonly NonDeletedExcalidrawElement[];
type Points = readonly Point[];
type Points = readonly LocalPoint[];
/** @returns vertices relative to element's top-left [0,0] position */
const getNonLinearElementRelativePoints = (
@ -26,20 +34,25 @@ const getNonLinearElementRelativePoints = (
Element,
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
>,
): [TopLeft: Point, TopRight: Point, BottomRight: Point, BottomLeft: Point] => {
): [
TopLeft: LocalPoint,
TopRight: LocalPoint,
BottomRight: LocalPoint,
BottomLeft: LocalPoint,
] => {
if (element.type === "diamond") {
return [
[element.width / 2, 0],
[element.width, element.height / 2],
[element.width / 2, element.height],
[0, element.height / 2],
pointFrom(element.width / 2, 0),
pointFrom(element.width, element.height / 2),
pointFrom(element.width / 2, element.height),
pointFrom(0, element.height / 2),
];
}
return [
[0, 0],
[0 + element.width, 0],
[0 + element.width, element.height],
[0, element.height],
pointFrom(0, 0),
pointFrom(0 + element.width, 0),
pointFrom(0 + element.width, element.height),
pointFrom(0, element.height),
];
};
@ -82,10 +95,10 @@ const getRotatedBBox = (element: Element): Bounds => {
const points = getElementRelativePoints(element);
const { cx, cy } = getMinMaxPoints(points);
const centerPoint: Point = [cx, cy];
const centerPoint = pointFrom<LocalPoint>(cx, cy);
const rotatedPoints = points.map((point) =>
rotatePoint([point[0], point[1]], centerPoint, element.angle),
const rotatedPoints = points.map((p) =>
pointRotateRads(p, centerPoint, element.angle),
);
const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
@ -133,10 +146,16 @@ export const elementPartiallyOverlapsWithOrContainsBBox = (
const elementBBox = getRotatedBBox(element);
return (
(isValueInRange(elementBBox[0], bbox[0], bbox[2]) ||
isValueInRange(bbox[0], elementBBox[0], elementBBox[2])) &&
(isValueInRange(elementBBox[1], bbox[1], bbox[3]) ||
isValueInRange(bbox[1], elementBBox[1], elementBBox[3]))
(rangeIncludesValue(elementBBox[0], rangeInclusive(bbox[0], bbox[2])) ||
rangeIncludesValue(
bbox[0],
rangeInclusive(elementBBox[0], elementBBox[2]),
)) &&
(rangeIncludesValue(elementBBox[1], rangeInclusive(bbox[1], bbox[3])) ||
rangeIncludesValue(
bbox[1],
rangeInclusive(elementBBox[1], elementBBox[3]),
))
);
};
@ -158,7 +177,7 @@ export const elementsOverlappingBBox = ({
type: "overlap" | "contain" | "inside";
}) => {
if (isExcalidrawElement(bounds)) {
bounds = getElementBounds(bounds);
bounds = getElementBounds(bounds, arrayToMap(elements));
}
const adjustedBBox: Bounds = [
bounds[0] - errorMargin,

View file

@ -5,17 +5,21 @@ exports[`exportToSvg > with default arguments 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
"fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
},
"collaborators": Map {},
"contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar",
"currentHoveredFontFamily": null,
"currentItemArrowType": "round",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFontFamily": 1,
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
@ -27,11 +31,10 @@ exports[`exportToSvg > with default arguments 1`] = `
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
"exportBackground": true,
@ -48,14 +51,19 @@ exports[`exportToSvg > with default arguments 1`] = `
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"hoveredElementIds": {},
"isBindingEnabled": true,
"isCropping": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "name",
"newElement": null,
"objectsSnapModeEnabled": false,
"openDialog": null,
"openMenu": null,
@ -77,6 +85,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"searchMatches": [],
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@ -84,10 +93,13 @@ exports[`exportToSvg > with default arguments 1`] = `
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": false,
"snapLines": [],
"startBoundElement": null,
"stats": {
"open": false,
"panels": 3,
},
"suggestedBindings": [],
"theme": "light",
"toast": null,

View file

@ -0,0 +1,90 @@
import {
curve,
degreesToRadians,
lineSegment,
lineSegmentRotate,
pointFrom,
pointRotateDegs,
} from "@excalidraw/math";
import type { Curve, Degrees, GlobalPoint } from "@excalidraw/math";
import { pointOnCurve, pointOnPolyline } from "../src/collision";
import type { Polyline } from "../src/shape";
describe("point and curve", () => {
const c: Curve<GlobalPoint> = curve(
pointFrom(1.4, 1.65),
pointFrom(1.9, 7.9),
pointFrom(5.9, 1.65),
pointFrom(6.44, 4.84),
);
it("point on curve", () => {
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
});
});
describe("point and polylines", () => {
const polyline: Polyline<GlobalPoint> = [
lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
];
it("point on the line", () => {
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
});
it("point on the line with rotation", () => {
const truePoints = [
pointFrom(1, 0),
pointFrom(1, 2),
pointFrom(2, 2),
pointFrom(2, 1),
pointFrom(3, 1),
];
truePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
});
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
falsePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
});
});
});

View file

@ -1,9 +1,9 @@
import * as utils from ".";
import { diagramFactory } from "../excalidraw/tests/fixtures/diagramFixture";
import { MIME_TYPES } from "@excalidraw/common";
import * as mockedSceneExportUtils from "@excalidraw/excalidraw/scene/export";
import { diagramFactory } from "@excalidraw/excalidraw/tests/fixtures/diagramFixture";
import { vi } from "vitest";
import * as mockedSceneExportUtils from "../excalidraw/scene/export";
import { MIME_TYPES } from "../excalidraw/constants";
import * as utils from "../src";
const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
@ -32,7 +32,6 @@ describe("exportToCanvas", async () => {
describe("exportToBlob", async () => {
describe("mime type", () => {
// afterEach(vi.restoreAllMocks);
it("should change image/jpg to image/jpeg", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),

View file

@ -0,0 +1,163 @@
import {
pointFrom,
lineSegment,
polygon,
pointOnLineSegment,
pointOnPolygon,
polygonIncludesPoint,
segmentsIntersectAt,
} from "@excalidraw/math";
import type {
GlobalPoint,
LineSegment,
Polygon,
Radians,
} from "@excalidraw/math";
import { pointInEllipse, pointOnEllipse, type Ellipse } from "../src/shape";
describe("point and line", () => {
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
// it("point on left or right of line", () => {
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
// });
const s: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 0),
pointFrom(1, 2),
);
it("point on the line", () => {
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
});
});
describe("point and polygon", () => {
const poly: Polygon<GlobalPoint> = polygon(
pointFrom(10, 10),
pointFrom(50, 10),
pointFrom(50, 50),
pointFrom(10, 50),
);
it("point on polygon", () => {
expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true);
expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false);
expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false);
});
it("point in polygon", () => {
const poly: Polygon<GlobalPoint> = polygon(
pointFrom(0, 0),
pointFrom(2, 0),
pointFrom(2, 2),
pointFrom(0, 2),
);
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
});
});
describe("point and ellipse", () => {
const ellipse: Ellipse<GlobalPoint> = {
center: pointFrom(0, 0),
angle: 0 as Radians,
halfWidth: 2,
halfHeight: 1,
};
it("point on ellipse", () => {
[
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointOnEllipse(p, ellipse)).toBe(true);
});
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
});
it("point in ellipse", () => {
[
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointInEllipse(p, ellipse)).toBe(true);
});
expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
});
});
describe("line and line", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 4),
pointFrom(3, 4),
);
const lineB: LineSegment<GlobalPoint> = lineSegment(
pointFrom(2, 1),
pointFrom(2, 7),
);
const lineC: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineD: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineE: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 9),
pointFrom(3, 9),
);
const lineF: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 2),
pointFrom(3, 4),
);
const lineG: LineSegment<GlobalPoint> = lineSegment(
pointFrom(0, 1),
pointFrom(2, 3),
);
it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
});
});

View file

@ -1,7 +1,10 @@
import { decodePngMetadata, decodeSvgMetadata } from "../excalidraw/data/image";
import { ImportedDataState } from "../excalidraw/data/types";
import * as utils from "../utils";
import { API } from "../excalidraw/tests/helpers/api";
import { decodePngMetadata } from "@excalidraw/excalidraw/data/image";
import { decodeSvgBase64Payload } from "@excalidraw/excalidraw/scene/export";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import * as utils from "../src";
// NOTE this test file is using the actual API, unmocked. Hence splitting it
// from the other test file, because I couldn't figure out how to test
@ -19,7 +22,7 @@ describe("embedding scene data", () => {
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
gridModeEnabled: false,
exportEmbedScene: true,
},
files: null,
@ -27,7 +30,7 @@ describe("embedding scene data", () => {
const svg = svgNode.outerHTML;
const parsedString = await decodeSvgMetadata({ svg });
const parsedString = decodeSvgBase64Payload({ svg });
const importedData: ImportedDataState = JSON.parse(parsedString);
expect(sourceElements.map((x) => x.id)).toEqual(
@ -50,7 +53,7 @@ describe("embedding scene data", () => {
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
gridModeEnabled: false,
exportEmbedScene: true,
},
files: null,

View file

@ -1,10 +1,12 @@
import { Bounds } from "../excalidraw/element/bounds";
import { API } from "../excalidraw/tests/helpers/api";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import type { Bounds } from "@excalidraw/element/bounds";
import {
elementPartiallyOverlapsWithOrContainsBBox,
elementsOverlappingBBox,
isElementInsideBBox,
} from "./withinBounds";
} from "../src/withinBounds";
const makeElement = (x: number, y: number, width: number, height: number) =>
API.createElement({

View file

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
}

View file

@ -1,60 +0,0 @@
const webpack = require("webpack");
const path = require("path");
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: { "excalidraw-utils.min": "./index.js" },
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
library: "ExcalidrawUtils",
libraryTarget: "umd",
},
resolve: {
extensions: [".tsx", ".ts", ".js", ".css", ".scss"],
},
optimization: {
runtimeChunk: false,
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
exclude: /node_modules/,
use: ["style-loader", { loader: "css-loader" }, "sass-loader"],
},
{
test: /\.(ts|tsx|js)$/,
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, "../tsconfig.prod.json"),
},
},
{
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
plugins: [["@babel/plugin-transform-runtime"]],
},
},
],
},
],
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []),
],
};