123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- /**
- * 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 {
- 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,
- };
- };
|