shape.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * this file defines pure geometric shapes
  3. *
  4. * for instance, a cubic bezier curve is specified by its four control points and
  5. * an ellipse is defined by its center, angle, semi major axis and semi minor axis
  6. * (but in semi-width and semi-height so it's more relevant to Excalidraw)
  7. *
  8. * the idea with pure shapes is so that we can provide collision and other geoemtric methods not depending on
  9. * the specifics of roughjs or elements in Excalidraw; instead, we can focus on the pure shapes themselves
  10. *
  11. * also included in this file are methods for converting an Excalidraw element or a Drawable from roughjs
  12. * to pure shapes
  13. */
  14. import { getElementAbsoluteCoords } from "../../excalidraw/element";
  15. import {
  16. ElementsMap,
  17. ExcalidrawDiamondElement,
  18. ExcalidrawElement,
  19. ExcalidrawEllipseElement,
  20. ExcalidrawEmbeddableElement,
  21. ExcalidrawFrameLikeElement,
  22. ExcalidrawFreeDrawElement,
  23. ExcalidrawIframeElement,
  24. ExcalidrawImageElement,
  25. ExcalidrawLinearElement,
  26. ExcalidrawRectangleElement,
  27. ExcalidrawSelectionElement,
  28. ExcalidrawTextElement,
  29. } from "../../excalidraw/element/types";
  30. import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
  31. import { pointsOnBezierCurves } from "points-on-curve";
  32. import type { Drawable, Op } from "roughjs/bin/core";
  33. // a point is specified by its coordinate (x, y)
  34. export type Point = [number, number];
  35. export type Vector = Point;
  36. // a line (segment) is defined by two endpoints
  37. export type Line = [Point, Point];
  38. // a polyline (made up term here) is a line consisting of other line segments
  39. // this corresponds to a straight line element in the editor but it could also
  40. // be used to model other elements
  41. export type Polyline = Line[];
  42. // cubic bezier curve with four control points
  43. export type Curve = [Point, Point, Point, Point];
  44. // a polycurve is a curve consisting of ther curves, this corresponds to a complex
  45. // curve on the canvas
  46. export type Polycurve = Curve[];
  47. // a polygon is a closed shape by connecting the given points
  48. // rectangles and diamonds are modelled by polygons
  49. export type Polygon = Point[];
  50. // an ellipse is specified by its center, angle, and its major and minor axes
  51. // but for the sake of simplicity, we've used halfWidth and halfHeight instead
  52. // in replace of semi major and semi minor axes
  53. export type Ellipse = {
  54. center: Point;
  55. angle: number;
  56. halfWidth: number;
  57. halfHeight: number;
  58. };
  59. export type GeometricShape =
  60. | {
  61. type: "line";
  62. data: Line;
  63. }
  64. | {
  65. type: "polygon";
  66. data: Polygon;
  67. }
  68. | {
  69. type: "curve";
  70. data: Curve;
  71. }
  72. | {
  73. type: "ellipse";
  74. data: Ellipse;
  75. }
  76. | {
  77. type: "polyline";
  78. data: Polyline;
  79. }
  80. | {
  81. type: "polycurve";
  82. data: Polycurve;
  83. };
  84. type RectangularElement =
  85. | ExcalidrawRectangleElement
  86. | ExcalidrawDiamondElement
  87. | ExcalidrawFrameLikeElement
  88. | ExcalidrawEmbeddableElement
  89. | ExcalidrawImageElement
  90. | ExcalidrawIframeElement
  91. | ExcalidrawTextElement
  92. | ExcalidrawSelectionElement;
  93. // polygon
  94. export const getPolygonShape = (
  95. element: RectangularElement,
  96. ): GeometricShape => {
  97. const { angle, width, height, x, y } = element;
  98. const angleInDegrees = angleToDegrees(angle);
  99. const cx = x + width / 2;
  100. const cy = y + height / 2;
  101. const center: Point = [cx, cy];
  102. let data: Polygon = [];
  103. if (element.type === "diamond") {
  104. data = [
  105. pointRotate([cx, y], angleInDegrees, center),
  106. pointRotate([x + width, cy], angleInDegrees, center),
  107. pointRotate([cx, y + height], angleInDegrees, center),
  108. pointRotate([x, cy], angleInDegrees, center),
  109. ] as Polygon;
  110. } else {
  111. data = [
  112. pointRotate([x, y], angleInDegrees, center),
  113. pointRotate([x + width, y], angleInDegrees, center),
  114. pointRotate([x + width, y + height], angleInDegrees, center),
  115. pointRotate([x, y + height], angleInDegrees, center),
  116. ] as Polygon;
  117. }
  118. return {
  119. type: "polygon",
  120. data,
  121. };
  122. };
  123. // return the selection box for an element, possibly rotated as well
  124. export const getSelectionBoxShape = (
  125. element: ExcalidrawElement,
  126. elementsMap: ElementsMap,
  127. padding = 10,
  128. ) => {
  129. let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
  130. element,
  131. elementsMap,
  132. true,
  133. );
  134. x1 -= padding;
  135. x2 += padding;
  136. y1 -= padding;
  137. y2 += padding;
  138. const angleInDegrees = angleToDegrees(element.angle);
  139. const center: Point = [cx, cy];
  140. const topLeft = pointRotate([x1, y1], angleInDegrees, center);
  141. const topRight = pointRotate([x2, y1], angleInDegrees, center);
  142. const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
  143. const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
  144. return {
  145. type: "polygon",
  146. data: [topLeft, topRight, bottomRight, bottomLeft],
  147. } as GeometricShape;
  148. };
  149. // ellipse
  150. export const getEllipseShape = (
  151. element: ExcalidrawEllipseElement,
  152. ): GeometricShape => {
  153. const { width, height, angle, x, y } = element;
  154. return {
  155. type: "ellipse",
  156. data: {
  157. center: [x + width / 2, y + height / 2],
  158. angle,
  159. halfWidth: width / 2,
  160. halfHeight: height / 2,
  161. },
  162. };
  163. };
  164. export const getCurvePathOps = (shape: Drawable): Op[] => {
  165. for (const set of shape.sets) {
  166. if (set.type === "path") {
  167. return set.ops;
  168. }
  169. }
  170. return shape.sets[0].ops;
  171. };
  172. // linear
  173. export const getCurveShape = (
  174. roughShape: Drawable,
  175. startingPoint: Point = [0, 0],
  176. angleInRadian: number,
  177. center: Point,
  178. ): GeometricShape => {
  179. const transform = (p: Point) =>
  180. pointRotate(
  181. [p[0] + startingPoint[0], p[1] + startingPoint[1]],
  182. angleToDegrees(angleInRadian),
  183. center,
  184. );
  185. const ops = getCurvePathOps(roughShape);
  186. const polycurve: Polycurve = [];
  187. let p0: Point = [0, 0];
  188. for (const op of ops) {
  189. if (op.op === "move") {
  190. p0 = transform(op.data as Point);
  191. }
  192. if (op.op === "bcurveTo") {
  193. const p1: Point = transform([op.data[0], op.data[1]]);
  194. const p2: Point = transform([op.data[2], op.data[3]]);
  195. const p3: Point = transform([op.data[4], op.data[5]]);
  196. polycurve.push([p0, p1, p2, p3]);
  197. p0 = p3;
  198. }
  199. }
  200. return {
  201. type: "polycurve",
  202. data: polycurve,
  203. };
  204. };
  205. const polylineFromPoints = (points: Point[]) => {
  206. let previousPoint = points[0];
  207. const polyline: Polyline = [];
  208. for (let i = 1; i < points.length; i++) {
  209. const nextPoint = points[i];
  210. polyline.push([previousPoint, nextPoint]);
  211. previousPoint = nextPoint;
  212. }
  213. return polyline;
  214. };
  215. export const getFreedrawShape = (
  216. element: ExcalidrawFreeDrawElement,
  217. center: Point,
  218. isClosed: boolean = false,
  219. ): GeometricShape => {
  220. const angle = angleToDegrees(element.angle);
  221. const transform = (p: Point) =>
  222. pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
  223. const polyline = polylineFromPoints(
  224. element.points.map((p) => transform(p as Point)),
  225. );
  226. return isClosed
  227. ? {
  228. type: "polygon",
  229. data: close(polyline.flat()) as Polygon,
  230. }
  231. : {
  232. type: "polyline",
  233. data: polyline,
  234. };
  235. };
  236. export const getClosedCurveShape = (
  237. element: ExcalidrawLinearElement,
  238. roughShape: Drawable,
  239. startingPoint: Point = [0, 0],
  240. angleInRadian: number,
  241. center: Point,
  242. ): GeometricShape => {
  243. const transform = (p: Point) =>
  244. pointRotate(
  245. [p[0] + startingPoint[0], p[1] + startingPoint[1]],
  246. angleToDegrees(angleInRadian),
  247. center,
  248. );
  249. if (element.roundness === null) {
  250. return {
  251. type: "polygon",
  252. data: close(element.points.map((p) => transform(p as Point))),
  253. };
  254. }
  255. const ops = getCurvePathOps(roughShape);
  256. const points: Point[] = [];
  257. let odd = false;
  258. for (const operation of ops) {
  259. if (operation.op === "move") {
  260. odd = !odd;
  261. if (odd) {
  262. points.push([operation.data[0], operation.data[1]]);
  263. }
  264. } else if (operation.op === "bcurveTo") {
  265. if (odd) {
  266. points.push([operation.data[0], operation.data[1]]);
  267. points.push([operation.data[2], operation.data[3]]);
  268. points.push([operation.data[4], operation.data[5]]);
  269. }
  270. } else if (operation.op === "lineTo") {
  271. if (odd) {
  272. points.push([operation.data[0], operation.data[1]]);
  273. }
  274. }
  275. }
  276. const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
  277. transform(p),
  278. );
  279. return {
  280. type: "polygon",
  281. data: polygonPoints,
  282. };
  283. };