You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

intersections.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import { Bounds } from 'types'
  2. import vec from 'utils/vec'
  3. import { isAngleBetween } from './utils'
  4. interface Intersection {
  5. didIntersect: boolean
  6. message: string
  7. points: number[][]
  8. }
  9. function getIntersection(message: string, ...points: number[][]) {
  10. return { didIntersect: points.length > 0, message, points }
  11. }
  12. export function intersectRays(
  13. p0: number[],
  14. n0: number[],
  15. p1: number[],
  16. n1: number[]
  17. ): Intersection {
  18. const dx = p1[0] - p0[0]
  19. const dy = p1[1] - p0[1]
  20. const det = n1[0] * n0[1] - n1[1] * n0[0]
  21. const u = (dy * n1[0] - dx * n1[1]) / det
  22. const v = (dy * n0[0] - dx * n0[1]) / det
  23. if (u < 0 || v < 0) return getIntersection('miss')
  24. const m0 = n0[1] / n0[0]
  25. const m1 = n1[1] / n1[0]
  26. const b0 = p0[1] - m0 * p0[0]
  27. const b1 = p1[1] - m1 * p1[0]
  28. const x = (b1 - b0) / (m0 - m1)
  29. const y = m0 * x + b0
  30. return Number.isFinite(x)
  31. ? getIntersection('intersection', [x, y])
  32. : getIntersection('parallel')
  33. }
  34. export function intersectLineSegments(
  35. a1: number[],
  36. a2: number[],
  37. b1: number[],
  38. b2: number[]
  39. ): Intersection {
  40. const AB = vec.sub(a1, b1)
  41. const BV = vec.sub(b2, b1)
  42. const AV = vec.sub(a2, a1)
  43. const ua_t = BV[0] * AB[1] - BV[1] * AB[0]
  44. const ub_t = AV[0] * AB[1] - AV[1] * AB[0]
  45. const u_b = BV[1] * AV[0] - BV[0] * AV[1]
  46. if (ua_t === 0 || ub_t === 0) {
  47. return getIntersection('coincident')
  48. }
  49. if (u_b === 0) {
  50. return getIntersection('parallel')
  51. }
  52. if (u_b != 0) {
  53. const ua = ua_t / u_b
  54. const ub = ub_t / u_b
  55. if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
  56. return getIntersection('intersection', vec.add(a1, vec.mul(AV, ua)))
  57. }
  58. }
  59. return getIntersection('no intersection')
  60. }
  61. export function intersectCircleCircle(a: number[], b: number[]): Intersection {
  62. const R = a[2],
  63. r = b[2]
  64. let dx = b[0] - a[0],
  65. dy = b[1] - a[1]
  66. const d = Math.sqrt(dx * dx + dy * dy),
  67. x = (d * d - r * r + R * R) / (2 * d),
  68. y = Math.sqrt(R * R - x * x)
  69. dx /= d
  70. dy /= d
  71. return getIntersection(
  72. 'intersection',
  73. [a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
  74. [a[0] + dx * x + dy * y, a[1] + dy * x - dx * y]
  75. )
  76. }
  77. export function intersectCircleLineSegment(
  78. c: number[],
  79. r: number,
  80. a1: number[],
  81. a2: number[]
  82. ): Intersection {
  83. const a =
  84. (a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
  85. const b =
  86. 2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
  87. const cc =
  88. c[0] * c[0] +
  89. c[1] * c[1] +
  90. a1[0] * a1[0] +
  91. a1[1] * a1[1] -
  92. 2 * (c[0] * a1[0] + c[1] * a1[1]) -
  93. r * r
  94. const deter = b * b - 4 * a * cc
  95. if (deter < 0) {
  96. return getIntersection('outside')
  97. }
  98. if (deter === 0) {
  99. return getIntersection('tangent')
  100. }
  101. const e = Math.sqrt(deter)
  102. const u1 = (-b + e) / (2 * a)
  103. const u2 = (-b - e) / (2 * a)
  104. if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
  105. if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
  106. return getIntersection('outside')
  107. } else {
  108. return getIntersection('inside')
  109. }
  110. }
  111. const results: number[][] = []
  112. if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
  113. if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
  114. return getIntersection('intersection', ...results)
  115. }
  116. export function intersectEllipseLineSegment(
  117. center: number[],
  118. rx: number,
  119. ry: number,
  120. a1: number[],
  121. a2: number[],
  122. rotation = 0
  123. ): Intersection {
  124. // If the ellipse or line segment are empty, return no tValues.
  125. if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
  126. return getIntersection('No intersection')
  127. }
  128. // Get the semimajor and semiminor axes.
  129. rx = rx < 0 ? rx : -rx
  130. ry = ry < 0 ? ry : -ry
  131. // Rotate points and translate so the ellipse is centered at the origin.
  132. a1 = vec.sub(vec.rotWith(a1, center, -rotation), center)
  133. a2 = vec.sub(vec.rotWith(a2, center, -rotation), center)
  134. // Calculate the quadratic parameters.
  135. const diff = vec.sub(a2, a1)
  136. const A = (diff[0] * diff[0]) / rx / rx + (diff[1] * diff[1]) / ry / ry
  137. const B = (2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry
  138. const C = (a1[0] * a1[0]) / rx / rx + (a1[1] * a1[1]) / ry / ry - 1
  139. // Make a list of t values (normalized points on the line where intersections occur).
  140. const tValues: number[] = []
  141. // Calculate the discriminant.
  142. const discriminant = B * B - 4 * A * C
  143. if (discriminant === 0) {
  144. // One real solution.
  145. tValues.push(-B / 2 / A)
  146. } else if (discriminant > 0) {
  147. const root = Math.sqrt(discriminant)
  148. // Two real solutions.
  149. tValues.push((-B + root) / 2 / A)
  150. tValues.push((-B - root) / 2 / A)
  151. }
  152. // Filter to only points that are on the segment.
  153. // Solve for points, then counter-rotate points.
  154. const points = tValues
  155. .filter((t) => t >= 0 && t <= 1)
  156. .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
  157. .map((p) => vec.rotWith(p, center, rotation))
  158. return getIntersection('intersection', ...points)
  159. }
  160. export function intersectArcLineSegment(
  161. start: number[],
  162. end: number[],
  163. center: number[],
  164. radius: number,
  165. A: number[],
  166. B: number[]
  167. ): Intersection {
  168. const sa = vec.angle(center, start)
  169. const ea = vec.angle(center, end)
  170. const ellipseTest = intersectEllipseLineSegment(center, radius, radius, A, B)
  171. if (!ellipseTest.didIntersect) return getIntersection('No intersection')
  172. const points = ellipseTest.points.filter((point) =>
  173. isAngleBetween(sa, ea, vec.angle(center, point))
  174. )
  175. if (points.length === 0) {
  176. return getIntersection('No intersection')
  177. }
  178. return getIntersection('intersection', ...points)
  179. }
  180. export function intersectCircleRectangle(
  181. c: number[],
  182. r: number,
  183. point: number[],
  184. size: number[]
  185. ): Intersection[] {
  186. const tl = point
  187. const tr = vec.add(point, [size[0], 0])
  188. const br = vec.add(point, size)
  189. const bl = vec.add(point, [0, size[1]])
  190. const intersections: Intersection[] = []
  191. const topIntersection = intersectCircleLineSegment(c, r, tl, tr)
  192. const rightIntersection = intersectCircleLineSegment(c, r, tr, br)
  193. const bottomIntersection = intersectCircleLineSegment(c, r, bl, br)
  194. const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
  195. if (topIntersection.didIntersect) {
  196. intersections.push({ ...topIntersection, message: 'top' })
  197. }
  198. if (rightIntersection.didIntersect) {
  199. intersections.push({ ...rightIntersection, message: 'right' })
  200. }
  201. if (bottomIntersection.didIntersect) {
  202. intersections.push({ ...bottomIntersection, message: 'bottom' })
  203. }
  204. if (leftIntersection.didIntersect) {
  205. intersections.push({ ...leftIntersection, message: 'left' })
  206. }
  207. return intersections
  208. }
  209. export function intersectEllipseRectangle(
  210. c: number[],
  211. rx: number,
  212. ry: number,
  213. point: number[],
  214. size: number[],
  215. rotation = 0
  216. ): Intersection[] {
  217. const tl = point
  218. const tr = vec.add(point, [size[0], 0])
  219. const br = vec.add(point, size)
  220. const bl = vec.add(point, [0, size[1]])
  221. const intersections: Intersection[] = []
  222. const topIntersection = intersectEllipseLineSegment(
  223. c,
  224. rx,
  225. ry,
  226. tl,
  227. tr,
  228. rotation
  229. )
  230. const rightIntersection = intersectEllipseLineSegment(
  231. c,
  232. rx,
  233. ry,
  234. tr,
  235. br,
  236. rotation
  237. )
  238. const bottomIntersection = intersectEllipseLineSegment(
  239. c,
  240. rx,
  241. ry,
  242. bl,
  243. br,
  244. rotation
  245. )
  246. const leftIntersection = intersectEllipseLineSegment(
  247. c,
  248. rx,
  249. ry,
  250. tl,
  251. bl,
  252. rotation
  253. )
  254. if (topIntersection.didIntersect) {
  255. intersections.push({ ...topIntersection, message: 'top' })
  256. }
  257. if (rightIntersection.didIntersect) {
  258. intersections.push({ ...rightIntersection, message: 'right' })
  259. }
  260. if (bottomIntersection.didIntersect) {
  261. intersections.push({ ...bottomIntersection, message: 'bottom' })
  262. }
  263. if (leftIntersection.didIntersect) {
  264. intersections.push({ ...leftIntersection, message: 'left' })
  265. }
  266. return intersections
  267. }
  268. export function intersectRectangleLineSegment(
  269. point: number[],
  270. size: number[],
  271. a1: number[],
  272. a2: number[]
  273. ): Intersection[] {
  274. const tl = point
  275. const tr = vec.add(point, [size[0], 0])
  276. const br = vec.add(point, size)
  277. const bl = vec.add(point, [0, size[1]])
  278. const intersections: Intersection[] = []
  279. const topIntersection = intersectLineSegments(a1, a2, tl, tr)
  280. const rightIntersection = intersectLineSegments(a1, a2, tr, br)
  281. const bottomIntersection = intersectLineSegments(a1, a2, bl, br)
  282. const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
  283. if (topIntersection.didIntersect) {
  284. intersections.push({ ...topIntersection, message: 'top' })
  285. }
  286. if (rightIntersection.didIntersect) {
  287. intersections.push({ ...rightIntersection, message: 'right' })
  288. }
  289. if (bottomIntersection.didIntersect) {
  290. intersections.push({ ...bottomIntersection, message: 'bottom' })
  291. }
  292. if (leftIntersection.didIntersect) {
  293. intersections.push({ ...leftIntersection, message: 'left' })
  294. }
  295. return intersections
  296. }
  297. export function intersectArcRectangle(
  298. start: number[],
  299. end: number[],
  300. center: number[],
  301. radius: number,
  302. point: number[],
  303. size: number[]
  304. ): Intersection[] {
  305. const tl = point
  306. const tr = vec.add(point, [size[0], 0])
  307. const br = vec.add(point, size)
  308. const bl = vec.add(point, [0, size[1]])
  309. const intersections: Intersection[] = []
  310. const topIntersection = intersectArcLineSegment(
  311. start,
  312. end,
  313. center,
  314. radius,
  315. tl,
  316. tr
  317. )
  318. const rightIntersection = intersectArcLineSegment(
  319. start,
  320. end,
  321. center,
  322. radius,
  323. tr,
  324. br
  325. )
  326. const bottomIntersection = intersectArcLineSegment(
  327. start,
  328. end,
  329. center,
  330. radius,
  331. bl,
  332. br
  333. )
  334. const leftIntersection = intersectArcLineSegment(
  335. start,
  336. end,
  337. center,
  338. radius,
  339. tl,
  340. bl
  341. )
  342. if (topIntersection.didIntersect) {
  343. intersections.push({ ...topIntersection, message: 'top' })
  344. }
  345. if (rightIntersection.didIntersect) {
  346. intersections.push({ ...rightIntersection, message: 'right' })
  347. }
  348. if (bottomIntersection.didIntersect) {
  349. intersections.push({ ...bottomIntersection, message: 'bottom' })
  350. }
  351. if (leftIntersection.didIntersect) {
  352. intersections.push({ ...leftIntersection, message: 'left' })
  353. }
  354. return intersections
  355. }
  356. /* -------------------------------------------------- */
  357. /* Shape vs. Bounds */
  358. /* -------------------------------------------------- */
  359. export function intersectCircleBounds(
  360. c: number[],
  361. r: number,
  362. bounds: Bounds
  363. ): Intersection[] {
  364. const { minX, minY, width, height } = bounds
  365. return intersectCircleRectangle(c, r, [minX, minY], [width, height])
  366. }
  367. export function intersectEllipseBounds(
  368. c: number[],
  369. rx: number,
  370. ry: number,
  371. bounds: Bounds,
  372. rotation = 0
  373. ): Intersection[] {
  374. const { minX, minY, width, height } = bounds
  375. return intersectEllipseRectangle(
  376. c,
  377. rx,
  378. ry,
  379. [minX, minY],
  380. [width, height],
  381. rotation
  382. )
  383. }
  384. export function intersectLineSegmentBounds(
  385. a1: number[],
  386. a2: number[],
  387. bounds: Bounds
  388. ): Intersection[] {
  389. const { minX, minY, width, height } = bounds
  390. return intersectRectangleLineSegment([minX, minY], [width, height], a1, a2)
  391. }
  392. export function intersectPolylineBounds(
  393. points: number[][],
  394. bounds: Bounds
  395. ): Intersection[] {
  396. const { minX, minY, width, height } = bounds
  397. const intersections: Intersection[] = []
  398. for (let i = 1; i < points.length; i++) {
  399. intersections.push(
  400. ...intersectRectangleLineSegment(
  401. [minX, minY],
  402. [width, height],
  403. points[i - 1],
  404. points[i]
  405. )
  406. )
  407. }
  408. return intersections
  409. }
  410. export function intersectPolygonBounds(
  411. points: number[][],
  412. bounds: Bounds
  413. ): Intersection[] {
  414. const { minX, minY, width, height } = bounds
  415. const intersections: Intersection[] = []
  416. for (let i = 1; i < points.length + 1; i++) {
  417. intersections.push(
  418. ...intersectRectangleLineSegment(
  419. [minX, minY],
  420. [width, height],
  421. points[i - 1],
  422. points[i % points.length]
  423. )
  424. )
  425. }
  426. return intersections
  427. }
  428. export function intersectArcBounds(
  429. start: number[],
  430. end: number[],
  431. center: number[],
  432. radius: number,
  433. bounds: Bounds
  434. ): Intersection[] {
  435. const { minX, minY, width, height } = bounds
  436. return intersectArcRectangle(
  437. start,
  438. end,
  439. center,
  440. radius,
  441. [minX, minY],
  442. [width, height]
  443. )
  444. }