Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

intersections.ts 11KB

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