feat: Merge upstream/master into b310-digital/excalidraw#master (#3)
This commit is contained in:
@ -12,10 +12,12 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"currentChartType": "bar",
|
||||
"currentHoveredFontFamily": null,
|
||||
"currentItemArrowType": "round",
|
||||
"currentItemBackgroundColor": "transparent",
|
||||
"currentItemEndArrowhead": "arrow",
|
||||
"currentItemFillStyle": "solid",
|
||||
"currentItemFontFamily": 1,
|
||||
"currentItemFontFamily": 5,
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
@ -27,7 +29,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@ -48,7 +49,9 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"outline": true,
|
||||
},
|
||||
"frameToHighlight": null,
|
||||
"gridSize": null,
|
||||
"gridModeEnabled": false,
|
||||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"isBindingEnabled": true,
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
@ -56,6 +59,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "name",
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"openDialog": null,
|
||||
"openMenu": null,
|
||||
@ -84,10 +88,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,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Bounds } from "../excalidraw/element/bounds";
|
||||
import { Point } from "../excalidraw/types";
|
||||
import type { Bounds } from "../excalidraw/element/bounds";
|
||||
import type { Point } from "../excalidraw/types";
|
||||
|
||||
export type LineSegment = [Point, Point];
|
||||
|
||||
|
66
packages/utils/collision.ts
Normal file
66
packages/utils/collision.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import type { Point, Polygon, GeometricShape } from "./geometry/shape";
|
||||
import {
|
||||
pointInEllipse,
|
||||
pointInPolygon,
|
||||
pointOnCurve,
|
||||
pointOnEllipse,
|
||||
pointOnLine,
|
||||
pointOnPolycurve,
|
||||
pointOnPolygon,
|
||||
pointOnPolyline,
|
||||
close,
|
||||
} from "./geometry/geometry";
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
export const isPointOnShape = (
|
||||
point: Point,
|
||||
shape: GeometricShape,
|
||||
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 pointOnLine(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: Point, shape: GeometricShape) => {
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return pointInPolygon(point, shape.data);
|
||||
case "line":
|
||||
return false;
|
||||
case "curve":
|
||||
return false;
|
||||
case "ellipse":
|
||||
return pointInEllipse(point, shape.data);
|
||||
case "polyline": {
|
||||
const polygon = close(shape.data.flat()) as Polygon;
|
||||
return pointInPolygon(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: Point, bounds: Polygon) => {
|
||||
return pointInPolygon(point, bounds);
|
||||
};
|
@ -3,8 +3,8 @@ import {
|
||||
exportToSvg as _exportToSvg,
|
||||
} from "../excalidraw/scene/export";
|
||||
import { getDefaultAppState } from "../excalidraw/appState";
|
||||
import { AppState, BinaryFiles } from "../excalidraw/types";
|
||||
import {
|
||||
import type { AppState, BinaryFiles } from "../excalidraw/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeleted,
|
||||
@ -166,9 +166,11 @@ export const exportToSvg = async ({
|
||||
exportPadding,
|
||||
renderEmbeddables,
|
||||
exportingFrame,
|
||||
skipInliningFonts,
|
||||
}: Omit<ExportOpts, "getDimensions"> & {
|
||||
exportPadding?: number;
|
||||
renderEmbeddables?: boolean;
|
||||
skipInliningFonts?: true;
|
||||
}): Promise<SVGSVGElement> => {
|
||||
const { elements: restoredElements, appState: restoredAppState } = restore(
|
||||
{ elements, appState },
|
||||
@ -184,6 +186,7 @@ export const exportToSvg = async ({
|
||||
return _exportToSvg(restoredElements, exportAppState, files, {
|
||||
exportingFrame,
|
||||
renderEmbeddables,
|
||||
skipInliningFonts,
|
||||
});
|
||||
};
|
||||
|
||||
@ -205,21 +208,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";
|
||||
|
249
packages/utils/geometry/geometry.test.ts
Normal file
249
packages/utils/geometry/geometry.test.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import {
|
||||
lineIntersectsLine,
|
||||
lineRotate,
|
||||
pointInEllipse,
|
||||
pointInPolygon,
|
||||
pointLeftofLine,
|
||||
pointOnCurve,
|
||||
pointOnEllipse,
|
||||
pointOnLine,
|
||||
pointOnPolygon,
|
||||
pointOnPolyline,
|
||||
pointRightofLine,
|
||||
pointRotate,
|
||||
} from "./geometry";
|
||||
import type { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
const line: Line = [
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
];
|
||||
|
||||
it("point on left or right of line", () => {
|
||||
expect(pointLeftofLine([0, 1], line)).toBe(true);
|
||||
expect(pointLeftofLine([1, 1], line)).toBe(false);
|
||||
expect(pointLeftofLine([2, 1], line)).toBe(false);
|
||||
|
||||
expect(pointRightofLine([0, 1], line)).toBe(false);
|
||||
expect(pointRightofLine([1, 1], line)).toBe(false);
|
||||
expect(pointRightofLine([2, 1], line)).toBe(true);
|
||||
});
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnLine([0, 1], line)).toBe(false);
|
||||
expect(pointOnLine([1, 1], line, 0)).toBe(true);
|
||||
expect(pointOnLine([2, 1], line)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline = [
|
||||
[
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
],
|
||||
[
|
||||
[2, 2],
|
||||
[2, 1],
|
||||
],
|
||||
[
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
],
|
||||
];
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline([1, 0], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([1, 2], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 2], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 1], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([3, 1], polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline([1, 1], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
|
||||
expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline([0, 1], polyline)).toBe(false);
|
||||
expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
[1, 0],
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
] as Point[];
|
||||
|
||||
truePoints.forEach((point) => {
|
||||
const rotation = Math.random() * 360;
|
||||
const rotatedPoint = pointRotate(point, rotation);
|
||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
||||
lineRotate(line, rotation, [0, 0]),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||
});
|
||||
|
||||
const falsePoints = [
|
||||
[0, 1],
|
||||
[2.1, 1.5],
|
||||
] as Point[];
|
||||
|
||||
falsePoints.forEach((point) => {
|
||||
const rotation = Math.random() * 360;
|
||||
const rotatedPoint = pointRotate(point, rotation);
|
||||
const rotatedPolyline: Polyline = polyline.map((line) =>
|
||||
lineRotate(line, rotation, [0, 0]),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polygon", () => {
|
||||
const polygon: Polygon = [
|
||||
[10, 10],
|
||||
[50, 10],
|
||||
[50, 50],
|
||||
[10, 50],
|
||||
];
|
||||
|
||||
it("point on polygon", () => {
|
||||
expect(pointOnPolygon([30, 10], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([50, 30], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([30, 50], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([10, 30], polygon)).toBe(true);
|
||||
expect(pointOnPolygon([30, 30], polygon)).toBe(false);
|
||||
expect(pointOnPolygon([30, 70], polygon)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in polygon", () => {
|
||||
const polygon: Polygon = [
|
||||
[0, 0],
|
||||
[2, 0],
|
||||
[2, 2],
|
||||
[0, 2],
|
||||
];
|
||||
expect(pointInPolygon([1, 1], polygon)).toBe(true);
|
||||
expect(pointInPolygon([3, 3], polygon)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and curve", () => {
|
||||
const curve: Curve = [
|
||||
[1.4, 1.65],
|
||||
[1.9, 7.9],
|
||||
[5.9, 1.65],
|
||||
[6.44, 4.84],
|
||||
];
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(curve[0], curve)).toBe(true);
|
||||
expect(pointOnCurve(curve[3], curve)).toBe(true);
|
||||
|
||||
expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
|
||||
expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
|
||||
expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and ellipse", () => {
|
||||
const ellipse: Ellipse = {
|
||||
center: [0, 0],
|
||||
angle: 0,
|
||||
halfWidth: 2,
|
||||
halfHeight: 1,
|
||||
};
|
||||
|
||||
it("point on ellipse", () => {
|
||||
[
|
||||
[0, 1],
|
||||
[0, -1],
|
||||
[2, 0],
|
||||
[-2, 0],
|
||||
].forEach((point) => {
|
||||
expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
|
||||
});
|
||||
expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([-1.4, 0.71], ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([1.4, 0.7], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([1.4, 0.71], ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([1, -0.86], ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse([-1, -0.86], ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse([-1, 0.8], ellipse)).toBe(false);
|
||||
expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in ellipse", () => {
|
||||
[
|
||||
[0, 1],
|
||||
[0, -1],
|
||||
[2, 0],
|
||||
[-2, 0],
|
||||
].forEach((point) => {
|
||||
expect(pointInEllipse(point as Point, ellipse)).toBe(true);
|
||||
});
|
||||
|
||||
expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
|
||||
expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
|
||||
|
||||
expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
|
||||
expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line and line", () => {
|
||||
const lineA: Line = [
|
||||
[1, 4],
|
||||
[3, 4],
|
||||
];
|
||||
const lineB: Line = [
|
||||
[2, 1],
|
||||
[2, 7],
|
||||
];
|
||||
const lineC: Line = [
|
||||
[1, 8],
|
||||
[3, 8],
|
||||
];
|
||||
const lineD: Line = [
|
||||
[1, 8],
|
||||
[3, 8],
|
||||
];
|
||||
const lineE: Line = [
|
||||
[1, 9],
|
||||
[3, 9],
|
||||
];
|
||||
const lineF: Line = [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
];
|
||||
const lineG: Line = [
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
];
|
||||
|
||||
it("intersection", () => {
|
||||
expect(lineIntersectsLine(lineA, lineB)).toBe(true);
|
||||
expect(lineIntersectsLine(lineA, lineC)).toBe(false);
|
||||
expect(lineIntersectsLine(lineB, lineC)).toBe(false);
|
||||
expect(lineIntersectsLine(lineC, lineD)).toBe(true);
|
||||
expect(lineIntersectsLine(lineE, lineD)).toBe(false);
|
||||
expect(lineIntersectsLine(lineF, lineG)).toBe(true);
|
||||
});
|
||||
});
|
1060
packages/utils/geometry/geometry.ts
Normal file
1060
packages/utils/geometry/geometry.ts
Normal file
File diff suppressed because it is too large
Load Diff
321
packages/utils/geometry/shape.ts
Normal file
321
packages/utils/geometry/shape.ts
Normal file
@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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 { getElementAbsoluteCoords } from "../../excalidraw/element";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawIframeElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "../../excalidraw/element/types";
|
||||
import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
|
||||
// a point is specified by its coordinate (x, y)
|
||||
export type Point = [number, number];
|
||||
export type Vector = Point;
|
||||
|
||||
// a line (segment) is defined by two endpoints
|
||||
export type Line = [Point, Point];
|
||||
|
||||
// a polyline (made up term here) is a line consisting of other line segments
|
||||
// this corresponds to a straight line element in the editor but it could also
|
||||
// be used to model other elements
|
||||
export type Polyline = Line[];
|
||||
|
||||
// cubic bezier curve with four control points
|
||||
export type Curve = [Point, Point, Point, Point];
|
||||
|
||||
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
|
||||
// curve on the canvas
|
||||
export type Polycurve = Curve[];
|
||||
|
||||
// a polygon is a closed shape by connecting the given points
|
||||
// rectangles and diamonds are modelled by polygons
|
||||
export type Polygon = Point[];
|
||||
|
||||
// an ellipse is specified by its center, angle, and its major and minor axes
|
||||
// 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 = {
|
||||
center: Point;
|
||||
angle: number;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
export type GeometricShape =
|
||||
| {
|
||||
type: "line";
|
||||
data: Line;
|
||||
}
|
||||
| {
|
||||
type: "polygon";
|
||||
data: Polygon;
|
||||
}
|
||||
| {
|
||||
type: "curve";
|
||||
data: Curve;
|
||||
}
|
||||
| {
|
||||
type: "ellipse";
|
||||
data: Ellipse;
|
||||
}
|
||||
| {
|
||||
type: "polyline";
|
||||
data: Polyline;
|
||||
}
|
||||
| {
|
||||
type: "polycurve";
|
||||
data: Polycurve;
|
||||
};
|
||||
|
||||
type RectangularElement =
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawFrameLikeElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawSelectionElement;
|
||||
|
||||
// polygon
|
||||
export const getPolygonShape = (
|
||||
element: RectangularElement,
|
||||
): GeometricShape => {
|
||||
const { angle, width, height, x, y } = element;
|
||||
const angleInDegrees = angleToDegrees(angle);
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
|
||||
const center: Point = [cx, cy];
|
||||
|
||||
let data: Polygon = [];
|
||||
|
||||
if (element.type === "diamond") {
|
||||
data = [
|
||||
pointRotate([cx, y], angleInDegrees, center),
|
||||
pointRotate([x + width, cy], angleInDegrees, center),
|
||||
pointRotate([cx, y + height], angleInDegrees, center),
|
||||
pointRotate([x, cy], angleInDegrees, center),
|
||||
] as Polygon;
|
||||
} else {
|
||||
data = [
|
||||
pointRotate([x, y], angleInDegrees, center),
|
||||
pointRotate([x + width, y], angleInDegrees, center),
|
||||
pointRotate([x + width, y + height], angleInDegrees, center),
|
||||
pointRotate([x, y + height], angleInDegrees, center),
|
||||
] as Polygon;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "polygon",
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
// return the selection box for an element, possibly rotated as well
|
||||
export const getSelectionBoxShape = (
|
||||
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: Point = [cx, cy];
|
||||
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
|
||||
const topRight = pointRotate([x2, y1], angleInDegrees, center);
|
||||
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
|
||||
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
|
||||
|
||||
return {
|
||||
type: "polygon",
|
||||
data: [topLeft, topRight, bottomRight, bottomLeft],
|
||||
} as GeometricShape;
|
||||
};
|
||||
|
||||
// ellipse
|
||||
export const getEllipseShape = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
): GeometricShape => {
|
||||
const { width, height, angle, x, y } = element;
|
||||
|
||||
return {
|
||||
type: "ellipse",
|
||||
data: {
|
||||
center: [x + width / 2, y + height / 2],
|
||||
angle,
|
||||
halfWidth: width / 2,
|
||||
halfHeight: height / 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
for (const set of shape.sets) {
|
||||
if (set.type === "path") {
|
||||
return set.ops;
|
||||
}
|
||||
}
|
||||
return shape.sets[0].ops;
|
||||
};
|
||||
|
||||
// linear
|
||||
export const getCurveShape = (
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = [0, 0],
|
||||
angleInRadian: number,
|
||||
center: Point,
|
||||
): GeometricShape => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(
|
||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
||||
angleToDegrees(angleInRadian),
|
||||
center,
|
||||
);
|
||||
|
||||
const ops = getCurvePathOps(roughShape);
|
||||
const polycurve: Polycurve = [];
|
||||
let p0: Point = [0, 0];
|
||||
|
||||
for (const op of ops) {
|
||||
if (op.op === "move") {
|
||||
p0 = transform(op.data as Point);
|
||||
}
|
||||
if (op.op === "bcurveTo") {
|
||||
const p1: Point = transform([op.data[0], op.data[1]]);
|
||||
const p2: Point = transform([op.data[2], op.data[3]]);
|
||||
const p3: Point = transform([op.data[4], op.data[5]]);
|
||||
polycurve.push([p0, p1, p2, p3]);
|
||||
p0 = p3;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "polycurve",
|
||||
data: polycurve,
|
||||
};
|
||||
};
|
||||
|
||||
const polylineFromPoints = (points: Point[]) => {
|
||||
let previousPoint = points[0];
|
||||
const polyline: Polyline = [];
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const nextPoint = points[i];
|
||||
polyline.push([previousPoint, nextPoint]);
|
||||
previousPoint = nextPoint;
|
||||
}
|
||||
|
||||
return polyline;
|
||||
};
|
||||
|
||||
export const getFreedrawShape = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
center: Point,
|
||||
isClosed: boolean = false,
|
||||
): GeometricShape => {
|
||||
const angle = angleToDegrees(element.angle);
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
|
||||
|
||||
const polyline = polylineFromPoints(
|
||||
element.points.map((p) => transform(p as Point)),
|
||||
);
|
||||
|
||||
return isClosed
|
||||
? {
|
||||
type: "polygon",
|
||||
data: close(polyline.flat()) as Polygon,
|
||||
}
|
||||
: {
|
||||
type: "polyline",
|
||||
data: polyline,
|
||||
};
|
||||
};
|
||||
|
||||
export const getClosedCurveShape = (
|
||||
element: ExcalidrawLinearElement,
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = [0, 0],
|
||||
angleInRadian: number,
|
||||
center: Point,
|
||||
): GeometricShape => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(
|
||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
||||
angleToDegrees(angleInRadian),
|
||||
center,
|
||||
);
|
||||
|
||||
if (element.roundness === null) {
|
||||
return {
|
||||
type: "polygon",
|
||||
data: close(element.points.map((p) => transform(p 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([operation.data[0], operation.data[1]]);
|
||||
}
|
||||
} else if (operation.op === "bcurveTo") {
|
||||
if (odd) {
|
||||
points.push([operation.data[0], operation.data[1]]);
|
||||
points.push([operation.data[2], operation.data[3]]);
|
||||
points.push([operation.data[4], operation.data[5]]);
|
||||
}
|
||||
} else if (operation.op === "lineTo") {
|
||||
if (odd) {
|
||||
points.push([operation.data[0], operation.data[1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
||||
transform(p),
|
||||
);
|
||||
|
||||
return {
|
||||
type: "polygon",
|
||||
data: polygonPoints,
|
||||
};
|
||||
};
|
3
packages/utils/global.d.ts
vendored
Normal file
3
packages/utils/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
import "../excalidraw/global";
|
||||
import "../excalidraw/css";
|
@ -1 +0,0 @@
|
||||
export * from "./export";
|
4
packages/utils/index.ts
Normal file
4
packages/utils/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./export";
|
||||
export * from "./withinBounds";
|
||||
export * from "./bbox";
|
||||
export { getCommonBounds } from "../excalidraw/element/bounds";
|
@ -1,7 +1,16 @@
|
||||
{
|
||||
"name": "@excalidraw/utils",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/excalidraw-utils.min.js",
|
||||
"main": "./dist/prod/index.js",
|
||||
"type": "module",
|
||||
"module": "./dist/prod/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
@ -33,29 +42,48 @@
|
||||
"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": "1.0.11",
|
||||
"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/core": "7.24.5",
|
||||
"@babel/plugin-transform-arrow-functions": "7.24.1",
|
||||
"@babel/plugin-transform-async-to-generator": "7.24.1",
|
||||
"@babel/plugin-transform-runtime": "7.24.3",
|
||||
"@babel/plugin-transform-typescript": "7.24.5",
|
||||
"@babel/preset-env": "7.24.5",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"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",
|
||||
"fonteditor-core": "2.4.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"sass-loader": "13.0.2",
|
||||
"ts-loader": "9.3.1",
|
||||
"typescript": "4.9.4",
|
||||
"wawoff2": "2.0.1",
|
||||
"webpack": "5.76.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.10.0"
|
||||
"webpack-cli": "4.10.0",
|
||||
"which": "4.0.0"
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rm -rf types && tsc",
|
||||
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
|
||||
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types",
|
||||
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
||||
"pack": "yarn build:umd && yarn pack"
|
||||
}
|
||||
|
16
packages/utils/tsconfig.json
Normal file
16
packages/utils/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": ["**/*.test.*", "**/tests/*", "types", "dist"]
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { decodePngMetadata, decodeSvgMetadata } from "../excalidraw/data/image";
|
||||
import { ImportedDataState } from "../excalidraw/data/types";
|
||||
import type { ImportedDataState } from "../excalidraw/data/types";
|
||||
import * as utils from "../utils";
|
||||
import { API } from "../excalidraw/tests/helpers/api";
|
||||
|
||||
@ -19,7 +19,7 @@ describe("embedding scene data", () => {
|
||||
elements: sourceElements,
|
||||
appState: {
|
||||
viewBackgroundColor: "#ffffff",
|
||||
gridSize: null,
|
||||
gridModeEnabled: false,
|
||||
exportEmbedScene: true,
|
||||
},
|
||||
files: null,
|
||||
@ -50,7 +50,7 @@ describe("embedding scene data", () => {
|
||||
elements: sourceElements,
|
||||
appState: {
|
||||
viewBackgroundColor: "#ffffff",
|
||||
gridSize: null,
|
||||
gridModeEnabled: false,
|
||||
exportEmbedScene: true,
|
||||
},
|
||||
files: null,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Bounds } from "../excalidraw/element/bounds";
|
||||
import type { Bounds } from "../excalidraw/element/bounds";
|
||||
import { API } from "../excalidraw/tests/helpers/api";
|
||||
import {
|
||||
elementPartiallyOverlapsWithOrContainsBBox,
|
||||
|
@ -13,7 +13,9 @@ import {
|
||||
} from "../excalidraw/element/typeChecks";
|
||||
import { isValueInRange, rotatePoint } from "../excalidraw/math";
|
||||
import type { Point } from "../excalidraw/types";
|
||||
import { Bounds, getElementBounds } from "../excalidraw/element/bounds";
|
||||
import type { Bounds } from "../excalidraw/element/bounds";
|
||||
import { getElementBounds } from "../excalidraw/element/bounds";
|
||||
import { arrayToMap } from "../excalidraw/utils";
|
||||
|
||||
type Element = NonDeletedExcalidrawElement;
|
||||
type Elements = readonly NonDeletedExcalidrawElement[];
|
||||
@ -158,7 +160,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,
|
||||
|
Reference in New Issue
Block a user