2
0

geometry.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. import { distance2d } from "../../excalidraw/math";
  2. import {
  3. Point,
  4. Line,
  5. Polygon,
  6. Curve,
  7. Ellipse,
  8. Polycurve,
  9. Polyline,
  10. } from "./shape";
  11. const DEFAULT_THRESHOLD = 10e-5;
  12. /**
  13. * utils
  14. */
  15. // the two vectors are ao and bo
  16. export const cross = (a: Point, b: Point, o: Point) => {
  17. return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
  18. };
  19. export const isClosed = (polygon: Polygon) => {
  20. const first = polygon[0];
  21. const last = polygon[polygon.length - 1];
  22. return first[0] === last[0] && first[1] === last[1];
  23. };
  24. export const close = (polygon: Polygon) => {
  25. return isClosed(polygon) ? polygon : [...polygon, polygon[0]];
  26. };
  27. /**
  28. * angles
  29. */
  30. // convert radians to degress
  31. export const angleToDegrees = (angle: number) => {
  32. return (angle * 180) / Math.PI;
  33. };
  34. // convert degrees to radians
  35. export const angleToRadians = (angle: number) => {
  36. return (angle / 180) * Math.PI;
  37. };
  38. // return the angle of reflection given an angle of incidence and a surface angle in degrees
  39. export const angleReflect = (incidenceAngle: number, surfaceAngle: number) => {
  40. const a = surfaceAngle * 2 - incidenceAngle;
  41. return a >= 360 ? a - 360 : a < 0 ? a + 360 : a;
  42. };
  43. /**
  44. * points
  45. */
  46. const rotate = (point: Point, angle: number): Point => {
  47. return [
  48. point[0] * Math.cos(angle) - point[1] * Math.sin(angle),
  49. point[0] * Math.sin(angle) + point[1] * Math.cos(angle),
  50. ];
  51. };
  52. const isOrigin = (point: Point) => {
  53. return point[0] === 0 && point[1] === 0;
  54. };
  55. // rotate a given point about a given origin at the given angle
  56. export const pointRotate = (
  57. point: Point,
  58. angle: number,
  59. origin?: Point,
  60. ): Point => {
  61. const r = angleToRadians(angle);
  62. if (!origin || isOrigin(origin)) {
  63. return rotate(point, r);
  64. }
  65. return rotate(point.map((c, i) => c - origin[i]) as Point, r).map(
  66. (c, i) => c + origin[i],
  67. ) as Point;
  68. };
  69. // translate a point by an angle (in degrees) and distance
  70. export const pointTranslate = (point: Point, angle = 0, distance = 0) => {
  71. const r = angleToRadians(angle);
  72. return [
  73. point[0] + distance * Math.cos(r),
  74. point[1] + distance * Math.sin(r),
  75. ] as Point;
  76. };
  77. export const pointInverse = (point: Point) => {
  78. return [-point[0], -point[1]] as Point;
  79. };
  80. export const pointAdd = (pointA: Point, pointB: Point): Point => {
  81. return [pointA[0] + pointB[0], pointA[1] + pointB[1]];
  82. };
  83. export const distanceToPoint = (p1: Point, p2: Point) => {
  84. return distance2d(...p1, ...p2);
  85. };
  86. /**
  87. * lines
  88. */
  89. // return the angle of a line, in degrees
  90. export const lineAngle = (line: Line) => {
  91. return angleToDegrees(
  92. Math.atan2(line[1][1] - line[0][1], line[1][0] - line[0][0]),
  93. );
  94. };
  95. // get the distance between the endpoints of a line segment
  96. export const lineLength = (line: Line) => {
  97. return Math.sqrt(
  98. Math.pow(line[1][0] - line[0][0], 2) + Math.pow(line[1][1] - line[0][1], 2),
  99. );
  100. };
  101. // get the midpoint of a line segment
  102. export const lineMidpoint = (line: Line) => {
  103. return [
  104. (line[0][0] + line[1][0]) / 2,
  105. (line[0][1] + line[1][1]) / 2,
  106. ] as Point;
  107. };
  108. // return the coordinates resulting from rotating the given line about an origin by an angle in degrees
  109. // note that when the origin is not given, the midpoint of the given line is used as the origin
  110. export const lineRotate = (line: Line, angle: number, origin?: Point): Line => {
  111. return line.map((point) =>
  112. pointRotate(point, angle, origin || lineMidpoint(line)),
  113. ) as Line;
  114. };
  115. // returns the coordinates resulting from translating a line by an angle in degrees and a distance.
  116. export const lineTranslate = (line: Line, angle: number, distance: number) => {
  117. return line.map((point) => pointTranslate(point, angle, distance));
  118. };
  119. export const lineInterpolate = (line: Line, clamp = false) => {
  120. const [[x1, y1], [x2, y2]] = line;
  121. return (t: number) => {
  122. const t0 = clamp ? (t < 0 ? 0 : t > 1 ? 1 : t) : t;
  123. return [(x2 - x1) * t0 + x1, (y2 - y1) * t0 + y1] as Point;
  124. };
  125. };
  126. /**
  127. * curves
  128. */
  129. function clone(p: Point): Point {
  130. return [...p] as Point;
  131. }
  132. export const curveToBezier = (
  133. pointsIn: readonly Point[],
  134. curveTightness = 0,
  135. ): Point[] => {
  136. const len = pointsIn.length;
  137. if (len < 3) {
  138. throw new Error("A curve must have at least three points.");
  139. }
  140. const out: Point[] = [];
  141. if (len === 3) {
  142. out.push(
  143. clone(pointsIn[0]),
  144. clone(pointsIn[1]),
  145. clone(pointsIn[2]),
  146. clone(pointsIn[2]),
  147. );
  148. } else {
  149. const points: Point[] = [];
  150. points.push(pointsIn[0], pointsIn[0]);
  151. for (let i = 1; i < pointsIn.length; i++) {
  152. points.push(pointsIn[i]);
  153. if (i === pointsIn.length - 1) {
  154. points.push(pointsIn[i]);
  155. }
  156. }
  157. const b: Point[] = [];
  158. const s = 1 - curveTightness;
  159. out.push(clone(points[0]));
  160. for (let i = 1; i + 2 < points.length; i++) {
  161. const cachedVertArray = points[i];
  162. b[0] = [cachedVertArray[0], cachedVertArray[1]];
  163. b[1] = [
  164. cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
  165. cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
  166. ];
  167. b[2] = [
  168. points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
  169. points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
  170. ];
  171. b[3] = [points[i + 1][0], points[i + 1][1]];
  172. out.push(b[1], b[2], b[3]);
  173. }
  174. }
  175. return out;
  176. };
  177. export const curveRotate = (curve: Curve, angle: number, origin: Point) => {
  178. return curve.map((p) => pointRotate(p, angle, origin));
  179. };
  180. export const cubicBezierPoint = (t: number, controlPoints: Curve): Point => {
  181. const [p0, p1, p2, p3] = controlPoints;
  182. const x =
  183. Math.pow(1 - t, 3) * p0[0] +
  184. 3 * Math.pow(1 - t, 2) * t * p1[0] +
  185. 3 * (1 - t) * Math.pow(t, 2) * p2[0] +
  186. Math.pow(t, 3) * p3[0];
  187. const y =
  188. Math.pow(1 - t, 3) * p0[1] +
  189. 3 * Math.pow(1 - t, 2) * t * p1[1] +
  190. 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
  191. Math.pow(t, 3) * p3[1];
  192. return [x, y];
  193. };
  194. const solveCubicEquation = (a: number, b: number, c: number, d: number) => {
  195. // This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
  196. const roots: number[] = [];
  197. const discriminant =
  198. 18 * a * b * c * d -
  199. 4 * Math.pow(b, 3) * d +
  200. Math.pow(b, 2) * Math.pow(c, 2) -
  201. 4 * a * Math.pow(c, 3) -
  202. 27 * Math.pow(a, 2) * Math.pow(d, 2);
  203. if (discriminant >= 0) {
  204. const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
  205. const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
  206. const root1 = (-b - C - D) / (3 * a);
  207. const root2 = (-b + (C + D) / 2) / (3 * a);
  208. const root3 = (-b + (C + D) / 2) / (3 * a);
  209. roots.push(root1, root2, root3);
  210. } else {
  211. const realPart = -b / (3 * a);
  212. const root1 =
  213. 2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
  214. const root2 =
  215. 2 *
  216. Math.sqrt(-b / (3 * a)) *
  217. Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
  218. const root3 =
  219. 2 *
  220. Math.sqrt(-b / (3 * a)) *
  221. Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
  222. roots.push(root1, root2, root3);
  223. }
  224. return roots;
  225. };
  226. const findClosestParameter = (point: Point, controlPoints: Curve) => {
  227. // This function finds the parameter t that minimizes the distance between the point
  228. // and any point on the cubic Bezier curve.
  229. const [p0, p1, p2, p3] = controlPoints;
  230. // Use the direct formula to find the parameter t
  231. const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
  232. const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
  233. const c = 3 * p1[0] - 3 * p0[0];
  234. const d = p0[0] - point[0];
  235. const rootsX = solveCubicEquation(a, b, c, d);
  236. // Do the same for the y-coordinate
  237. const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
  238. const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
  239. const g = 3 * p1[1] - 3 * p0[1];
  240. const h = p0[1] - point[1];
  241. const rootsY = solveCubicEquation(e, f, g, h);
  242. // Select the real root that is between 0 and 1 (inclusive)
  243. const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
  244. const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
  245. if (validRootsX.length === 0 || validRootsY.length === 0) {
  246. // No valid roots found, use the midpoint as a fallback
  247. return 0.5;
  248. }
  249. // Choose the parameter t that minimizes the distance
  250. let minDistance = Infinity;
  251. let closestT = 0;
  252. for (const rootX of validRootsX) {
  253. for (const rootY of validRootsY) {
  254. const distance = Math.sqrt(
  255. (rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
  256. );
  257. if (distance < minDistance) {
  258. minDistance = distance;
  259. closestT = (rootX + rootY) / 2; // Use the average for a smoother result
  260. }
  261. }
  262. }
  263. return closestT;
  264. };
  265. export const cubicBezierDistance = (point: Point, controlPoints: Curve) => {
  266. // Calculate the closest point on the Bezier curve to the given point
  267. const t = findClosestParameter(point, controlPoints);
  268. // Calculate the coordinates of the closest point on the curve
  269. const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
  270. // Calculate the distance between the given point and the closest point on the curve
  271. const distance = Math.sqrt(
  272. (point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
  273. );
  274. return distance;
  275. };
  276. /**
  277. * polygons
  278. */
  279. export const polygonRotate = (
  280. polygon: Polygon,
  281. angle: number,
  282. origin: Point,
  283. ) => {
  284. return polygon.map((p) => pointRotate(p, angle, origin));
  285. };
  286. export const polygonBounds = (polygon: Polygon) => {
  287. let xMin = Infinity;
  288. let xMax = -Infinity;
  289. let yMin = Infinity;
  290. let yMax = -Infinity;
  291. for (let i = 0, l = polygon.length; i < l; i++) {
  292. const p = polygon[i];
  293. const x = p[0];
  294. const y = p[1];
  295. if (x != null && isFinite(x) && y != null && isFinite(y)) {
  296. if (x < xMin) {
  297. xMin = x;
  298. }
  299. if (x > xMax) {
  300. xMax = x;
  301. }
  302. if (y < yMin) {
  303. yMin = y;
  304. }
  305. if (y > yMax) {
  306. yMax = y;
  307. }
  308. }
  309. }
  310. return [
  311. [xMin, yMin],
  312. [xMax, yMax],
  313. ] as [Point, Point];
  314. };
  315. export const polygonCentroid = (vertices: Point[]) => {
  316. let a = 0;
  317. let x = 0;
  318. let y = 0;
  319. const l = vertices.length;
  320. for (let i = 0; i < l; i++) {
  321. const s = i === l - 1 ? 0 : i + 1;
  322. const v0 = vertices[i];
  323. const v1 = vertices[s];
  324. const f = v0[0] * v1[1] - v1[0] * v0[1];
  325. a += f;
  326. x += (v0[0] + v1[0]) * f;
  327. y += (v0[1] + v1[1]) * f;
  328. }
  329. const d = a * 3;
  330. return [x / d, y / d] as Point;
  331. };
  332. export const polygonScale = (
  333. polygon: Polygon,
  334. scale: number,
  335. origin?: Point,
  336. ) => {
  337. if (!origin) {
  338. origin = polygonCentroid(polygon);
  339. }
  340. const p: Polygon = [];
  341. for (let i = 0, l = polygon.length; i < l; i++) {
  342. const v = polygon[i];
  343. const d = lineLength([origin, v]);
  344. const a = lineAngle([origin, v]);
  345. p[i] = pointTranslate(origin, a, d * scale);
  346. }
  347. return p;
  348. };
  349. export const polygonScaleX = (
  350. polygon: Polygon,
  351. scale: number,
  352. origin?: Point,
  353. ) => {
  354. if (!origin) {
  355. origin = polygonCentroid(polygon);
  356. }
  357. const p: Polygon = [];
  358. for (let i = 0, l = polygon.length; i < l; i++) {
  359. const v = polygon[i];
  360. const d = lineLength([origin, v]);
  361. const a = lineAngle([origin, v]);
  362. const t = pointTranslate(origin, a, d * scale);
  363. p[i] = [t[0], v[1]];
  364. }
  365. return p;
  366. };
  367. export const polygonScaleY = (
  368. polygon: Polygon,
  369. scale: number,
  370. origin?: Point,
  371. ) => {
  372. if (!origin) {
  373. origin = polygonCentroid(polygon);
  374. }
  375. const p: Polygon = [];
  376. for (let i = 0, l = polygon.length; i < l; i++) {
  377. const v = polygon[i];
  378. const d = lineLength([origin, v]);
  379. const a = lineAngle([origin, v]);
  380. const t = pointTranslate(origin, a, d * scale);
  381. p[i] = [v[0], t[1]];
  382. }
  383. return p;
  384. };
  385. export const polygonReflectX = (polygon: Polygon, reflectFactor = 1) => {
  386. const [[min], [max]] = polygonBounds(polygon);
  387. const p: Point[] = [];
  388. for (let i = 0, l = polygon.length; i < l; i++) {
  389. const [x, y] = polygon[i];
  390. const r: Point = [min + max - x, y];
  391. if (reflectFactor === 0) {
  392. p[i] = [x, y];
  393. } else if (reflectFactor === 1) {
  394. p[i] = r;
  395. } else {
  396. const t = lineInterpolate([[x, y], r]);
  397. p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
  398. }
  399. }
  400. return p;
  401. };
  402. export const polygonReflectY = (polygon: Polygon, reflectFactor = 1) => {
  403. const [[, min], [, max]] = polygonBounds(polygon);
  404. const p: Point[] = [];
  405. for (let i = 0, l = polygon.length; i < l; i++) {
  406. const [x, y] = polygon[i];
  407. const r: Point = [x, min + max - y];
  408. if (reflectFactor === 0) {
  409. p[i] = [x, y];
  410. } else if (reflectFactor === 1) {
  411. p[i] = r;
  412. } else {
  413. const t = lineInterpolate([[x, y], r]);
  414. p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
  415. }
  416. }
  417. return p;
  418. };
  419. export const polygonTranslate = (
  420. polygon: Polygon,
  421. angle: number,
  422. distance: number,
  423. ) => {
  424. return polygon.map((p) => pointTranslate(p, angle, distance));
  425. };
  426. /**
  427. * ellipses
  428. */
  429. export const ellipseAxes = (ellipse: Ellipse) => {
  430. const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
  431. const majorAxis = widthGreaterThanHeight
  432. ? ellipse.halfWidth * 2
  433. : ellipse.halfHeight * 2;
  434. const minorAxis = widthGreaterThanHeight
  435. ? ellipse.halfHeight * 2
  436. : ellipse.halfWidth * 2;
  437. return {
  438. majorAxis,
  439. minorAxis,
  440. };
  441. };
  442. export const ellipseFocusToCenter = (ellipse: Ellipse) => {
  443. const { majorAxis, minorAxis } = ellipseAxes(ellipse);
  444. return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
  445. };
  446. export const ellipseExtremes = (ellipse: Ellipse) => {
  447. const { center, angle } = ellipse;
  448. const { majorAxis, minorAxis } = ellipseAxes(ellipse);
  449. const cos = Math.cos(angle);
  450. const sin = Math.sin(angle);
  451. const sqSum = majorAxis ** 2 + minorAxis ** 2;
  452. const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
  453. const yMax = Math.sqrt((sqSum - sqDiff) / 2);
  454. const xAtYMax =
  455. (yMax * sqSum * sin * cos) /
  456. (majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
  457. const xMax = Math.sqrt((sqSum + sqDiff) / 2);
  458. const yAtXMax =
  459. (xMax * sqSum * sin * cos) /
  460. (majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
  461. return [
  462. pointAdd([xAtYMax, yMax], center),
  463. pointAdd(pointInverse([xAtYMax, yMax]), center),
  464. pointAdd([xMax, yAtXMax], center),
  465. pointAdd([xMax, yAtXMax], center),
  466. ];
  467. };
  468. export const pointRelativeToCenter = (
  469. point: Point,
  470. center: Point,
  471. angle: number,
  472. ): Point => {
  473. const translated = pointAdd(point, pointInverse(center));
  474. const rotated = pointRotate(translated, -angleToDegrees(angle));
  475. return rotated;
  476. };
  477. /**
  478. * relationships
  479. */
  480. const topPointFirst = (line: Line) => {
  481. return line[1][1] > line[0][1] ? line : [line[1], line[0]];
  482. };
  483. export const pointLeftofLine = (point: Point, line: Line) => {
  484. const t = topPointFirst(line);
  485. return cross(point, t[1], t[0]) < 0;
  486. };
  487. export const pointRightofLine = (point: Point, line: Line) => {
  488. const t = topPointFirst(line);
  489. return cross(point, t[1], t[0]) > 0;
  490. };
  491. export const distanceToSegment = (point: Point, line: Line) => {
  492. const [x, y] = point;
  493. const [[x1, y1], [x2, y2]] = line;
  494. const A = x - x1;
  495. const B = y - y1;
  496. const C = x2 - x1;
  497. const D = y2 - y1;
  498. const dot = A * C + B * D;
  499. const len_sq = C * C + D * D;
  500. let param = -1;
  501. if (len_sq !== 0) {
  502. param = dot / len_sq;
  503. }
  504. let xx;
  505. let yy;
  506. if (param < 0) {
  507. xx = x1;
  508. yy = y1;
  509. } else if (param > 1) {
  510. xx = x2;
  511. yy = y2;
  512. } else {
  513. xx = x1 + param * C;
  514. yy = y1 + param * D;
  515. }
  516. const dx = x - xx;
  517. const dy = y - yy;
  518. return Math.sqrt(dx * dx + dy * dy);
  519. };
  520. export const pointOnLine = (
  521. point: Point,
  522. line: Line,
  523. threshold = DEFAULT_THRESHOLD,
  524. ) => {
  525. const distance = distanceToSegment(point, line);
  526. if (distance === 0) {
  527. return true;
  528. }
  529. return distance < threshold;
  530. };
  531. export const pointOnPolyline = (
  532. point: Point,
  533. polyline: Polyline,
  534. threshold = DEFAULT_THRESHOLD,
  535. ) => {
  536. return polyline.some((line) => pointOnLine(point, line, threshold));
  537. };
  538. export const lineIntersectsLine = (lineA: Line, lineB: Line) => {
  539. const [[a0x, a0y], [a1x, a1y]] = lineA;
  540. const [[b0x, b0y], [b1x, b1y]] = lineB;
  541. // shared points
  542. if (a0x === b0x && a0y === b0y) {
  543. return true;
  544. }
  545. if (a1x === b1x && a1y === b1y) {
  546. return true;
  547. }
  548. // point on line
  549. if (pointOnLine(lineA[0], lineB) || pointOnLine(lineA[1], lineB)) {
  550. return true;
  551. }
  552. if (pointOnLine(lineB[0], lineA) || pointOnLine(lineB[1], lineA)) {
  553. return true;
  554. }
  555. const denom = (b1y - b0y) * (a1x - a0x) - (b1x - b0x) * (a1y - a0y);
  556. if (denom === 0) {
  557. return false;
  558. }
  559. const deltaY = a0y - b0y;
  560. const deltaX = a0x - b0x;
  561. const numer0 = (b1x - b0x) * deltaY - (b1y - b0y) * deltaX;
  562. const numer1 = (a1x - a0x) * deltaY - (a1y - a0y) * deltaX;
  563. const quotA = numer0 / denom;
  564. const quotB = numer1 / denom;
  565. return quotA > 0 && quotA < 1 && quotB > 0 && quotB < 1;
  566. };
  567. export const lineIntersectsPolygon = (line: Line, polygon: Polygon) => {
  568. let intersects = false;
  569. const closed = close(polygon);
  570. for (let i = 0, l = closed.length - 1; i < l; i++) {
  571. const v0 = closed[i];
  572. const v1 = closed[i + 1];
  573. if (
  574. lineIntersectsLine(line, [v0, v1]) ||
  575. (pointOnLine(v0, line) && pointOnLine(v1, line))
  576. ) {
  577. intersects = true;
  578. break;
  579. }
  580. }
  581. return intersects;
  582. };
  583. export const pointInBezierEquation = (
  584. p0: Point,
  585. p1: Point,
  586. p2: Point,
  587. p3: Point,
  588. [mx, my]: Point,
  589. lineThreshold: number,
  590. ) => {
  591. // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
  592. const equation = (t: number, idx: number) =>
  593. Math.pow(1 - t, 3) * p3[idx] +
  594. 3 * t * Math.pow(1 - t, 2) * p2[idx] +
  595. 3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
  596. p0[idx] * Math.pow(t, 3);
  597. const lineSegmentPoints: Point[] = [];
  598. let t = 0;
  599. while (t <= 1.0) {
  600. const tx = equation(t, 0);
  601. const ty = equation(t, 1);
  602. const diff = Math.sqrt(Math.pow(tx - mx, 2) + Math.pow(ty - my, 2));
  603. if (diff < lineThreshold) {
  604. return true;
  605. }
  606. lineSegmentPoints.push([tx, ty]);
  607. t += 0.1;
  608. }
  609. // check the distance from line segments to the given point
  610. return false;
  611. };
  612. export const cubicBezierEquation = (curve: Curve) => {
  613. const [p0, p1, p2, p3] = curve;
  614. // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
  615. return (t: number, idx: number) =>
  616. Math.pow(1 - t, 3) * p3[idx] +
  617. 3 * t * Math.pow(1 - t, 2) * p2[idx] +
  618. 3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
  619. p0[idx] * Math.pow(t, 3);
  620. };
  621. export const polyLineFromCurve = (curve: Curve, segments = 10): Polyline => {
  622. const equation = cubicBezierEquation(curve);
  623. let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
  624. const lineSegments: Polyline = [];
  625. let t = 0;
  626. const increment = 1 / segments;
  627. for (let i = 0; i < segments; i++) {
  628. t += increment;
  629. if (t <= 1) {
  630. const nextPoint: Point = [equation(t, 0), equation(t, 1)];
  631. lineSegments.push([startingPoint, nextPoint]);
  632. startingPoint = nextPoint;
  633. }
  634. }
  635. return lineSegments;
  636. };
  637. export const pointOnCurve = (
  638. point: Point,
  639. curve: Curve,
  640. threshold = DEFAULT_THRESHOLD,
  641. ) => {
  642. return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
  643. };
  644. export const pointOnPolycurve = (
  645. point: Point,
  646. polycurve: Polycurve,
  647. threshold = DEFAULT_THRESHOLD,
  648. ) => {
  649. return polycurve.some((curve) => pointOnCurve(point, curve, threshold));
  650. };
  651. export const pointInPolygon = (point: Point, polygon: Polygon) => {
  652. const x = point[0];
  653. const y = point[1];
  654. let inside = false;
  655. for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
  656. const xi = polygon[i][0];
  657. const yi = polygon[i][1];
  658. const xj = polygon[j][0];
  659. const yj = polygon[j][1];
  660. if (
  661. ((yi > y && yj <= y) || (yi <= y && yj > y)) &&
  662. x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
  663. ) {
  664. inside = !inside;
  665. }
  666. }
  667. return inside;
  668. };
  669. export const pointOnPolygon = (
  670. point: Point,
  671. polygon: Polygon,
  672. threshold = DEFAULT_THRESHOLD,
  673. ) => {
  674. let on = false;
  675. const closed = close(polygon);
  676. for (let i = 0, l = closed.length - 1; i < l; i++) {
  677. if (pointOnLine(point, [closed[i], closed[i + 1]], threshold)) {
  678. on = true;
  679. break;
  680. }
  681. }
  682. return on;
  683. };
  684. export const polygonInPolygon = (polygonA: Polygon, polygonB: Polygon) => {
  685. let inside = true;
  686. const closed = close(polygonA);
  687. for (let i = 0, l = closed.length - 1; i < l; i++) {
  688. const v0 = closed[i];
  689. // Points test
  690. if (!pointInPolygon(v0, polygonB)) {
  691. inside = false;
  692. break;
  693. }
  694. // Lines test
  695. if (lineIntersectsPolygon([v0, closed[i + 1]], polygonB)) {
  696. inside = false;
  697. break;
  698. }
  699. }
  700. return inside;
  701. };
  702. export const polygonIntersectPolygon = (
  703. polygonA: Polygon,
  704. polygonB: Polygon,
  705. ) => {
  706. let intersects = false;
  707. let onCount = 0;
  708. const closed = close(polygonA);
  709. for (let i = 0, l = closed.length - 1; i < l; i++) {
  710. const v0 = closed[i];
  711. const v1 = closed[i + 1];
  712. if (lineIntersectsPolygon([v0, v1], polygonB)) {
  713. intersects = true;
  714. break;
  715. }
  716. if (pointOnPolygon(v0, polygonB)) {
  717. ++onCount;
  718. }
  719. if (onCount === 2) {
  720. intersects = true;
  721. break;
  722. }
  723. }
  724. return intersects;
  725. };
  726. const distanceToEllipse = (point: Point, ellipse: Ellipse) => {
  727. const { angle, halfWidth, halfHeight, center } = ellipse;
  728. const a = halfWidth;
  729. const b = halfHeight;
  730. const [rotatedPointX, rotatedPointY] = pointRelativeToCenter(
  731. point,
  732. center,
  733. angle,
  734. );
  735. const px = Math.abs(rotatedPointX);
  736. const py = Math.abs(rotatedPointY);
  737. let tx = 0.707;
  738. let ty = 0.707;
  739. for (let i = 0; i < 3; i++) {
  740. const x = a * tx;
  741. const y = b * ty;
  742. const ex = ((a * a - b * b) * tx ** 3) / a;
  743. const ey = ((b * b - a * a) * ty ** 3) / b;
  744. const rx = x - ex;
  745. const ry = y - ey;
  746. const qx = px - ex;
  747. const qy = py - ey;
  748. const r = Math.hypot(ry, rx);
  749. const q = Math.hypot(qy, qx);
  750. tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
  751. ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
  752. const t = Math.hypot(ty, tx);
  753. tx /= t;
  754. ty /= t;
  755. }
  756. const [minX, minY] = [
  757. a * tx * Math.sign(rotatedPointX),
  758. b * ty * Math.sign(rotatedPointY),
  759. ];
  760. return distanceToPoint([rotatedPointX, rotatedPointY], [minX, minY]);
  761. };
  762. export const pointOnEllipse = (
  763. point: Point,
  764. ellipse: Ellipse,
  765. threshold = DEFAULT_THRESHOLD,
  766. ) => {
  767. return distanceToEllipse(point, ellipse) <= threshold;
  768. };
  769. export const pointInEllipse = (point: Point, ellipse: Ellipse) => {
  770. const { center, angle, halfWidth, halfHeight } = ellipse;
  771. const [rotatedPointX, rotatedPointY] = pointRelativeToCenter(
  772. point,
  773. center,
  774. angle,
  775. );
  776. return (
  777. (rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
  778. (rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
  779. 1
  780. );
  781. };