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 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import { Bounds } from "types"
  2. import * as vec from "utils/vec"
  3. interface Intersection {
  4. didIntersect: boolean
  5. message: string
  6. points: number[][]
  7. }
  8. function getIntersection(message: string, ...points: number[][]) {
  9. return { didIntersect: points.length > 0, message, points }
  10. }
  11. export function intersectLineSegments(
  12. a1: number[],
  13. a2: number[],
  14. b1: number[],
  15. b2: number[]
  16. ) {
  17. const AB = vec.sub(a1, b1)
  18. const BV = vec.sub(b2, b1)
  19. const AV = vec.sub(a2, a1)
  20. const ua_t = BV[0] * AB[1] - BV[1] * AB[0]
  21. const ub_t = AV[0] * AB[1] - AV[1] * AB[0]
  22. const u_b = BV[1] * AV[0] - BV[0] * AV[1]
  23. if (ua_t === 0 || ub_t === 0) {
  24. return getIntersection("coincident")
  25. }
  26. if (u_b === 0) {
  27. return getIntersection("parallel")
  28. }
  29. if (u_b != 0) {
  30. const ua = ua_t / u_b
  31. const ub = ub_t / u_b
  32. if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
  33. return getIntersection("intersection", vec.add(a1, vec.mul(AV, ua)))
  34. }
  35. }
  36. return getIntersection("no intersection")
  37. }
  38. export function intersectCircleLineSegment(
  39. c: number[],
  40. r: number,
  41. a1: number[],
  42. a2: number[]
  43. ): Intersection {
  44. const a =
  45. (a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
  46. const b =
  47. 2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
  48. const cc =
  49. c[0] * c[0] +
  50. c[1] * c[1] +
  51. a1[0] * a1[0] +
  52. a1[1] * a1[1] -
  53. 2 * (c[0] * a1[0] + c[1] * a1[1]) -
  54. r * r
  55. const deter = b * b - 4 * a * cc
  56. if (deter < 0) {
  57. return getIntersection("outside")
  58. }
  59. if (deter === 0) {
  60. return getIntersection("tangent")
  61. }
  62. var e = Math.sqrt(deter)
  63. var u1 = (-b + e) / (2 * a)
  64. var u2 = (-b - e) / (2 * a)
  65. if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
  66. if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
  67. return getIntersection("outside")
  68. } else {
  69. return getIntersection("inside")
  70. }
  71. }
  72. const results: number[][] = []
  73. if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
  74. if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
  75. return getIntersection("intersection", ...results)
  76. }
  77. export function intersectEllipseLineSegment(
  78. center: number[],
  79. rx: number,
  80. ry: number,
  81. a1: number[],
  82. a2: number[],
  83. rotation = 0
  84. ) {
  85. // If the ellipse or line segment are empty, return no tValues.
  86. if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
  87. return getIntersection("No intersection")
  88. }
  89. // Get the semimajor and semiminor axes.
  90. rx = rx < 0 ? rx : -rx
  91. ry = ry < 0 ? ry : -ry
  92. // Rotate points and translate so the ellipse is centered at the origin.
  93. a1 = vec.sub(vec.rotWith(a1, center, -rotation), center)
  94. a2 = vec.sub(vec.rotWith(a2, center, -rotation), center)
  95. // Calculate the quadratic parameters.
  96. const diff = vec.sub(a2, a1)
  97. var A = (diff[0] * diff[0]) / rx / rx + (diff[1] * diff[1]) / ry / ry
  98. var B = (2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry
  99. var C = (a1[0] * a1[0]) / rx / rx + (a1[1] * a1[1]) / ry / ry - 1
  100. // Make a list of t values (normalized points on the line where intersections occur).
  101. var tValues: number[] = []
  102. // Calculate the discriminant.
  103. var discriminant = B * B - 4 * A * C
  104. if (discriminant === 0) {
  105. // One real solution.
  106. tValues.push(-B / 2 / A)
  107. } else if (discriminant > 0) {
  108. const root = Math.sqrt(discriminant)
  109. // Two real solutions.
  110. tValues.push((-B + root) / 2 / A)
  111. tValues.push((-B - root) / 2 / A)
  112. }
  113. // Filter to only points that are on the segment.
  114. // Solve for points, then counter-rotate points.
  115. const points = tValues
  116. .filter((t) => t >= 0 && t <= 1)
  117. .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
  118. .map((p) => vec.rotWith(p, center, rotation))
  119. return getIntersection("intersection", ...points)
  120. }
  121. export function intersectCircleRectangle(
  122. c: number[],
  123. r: number,
  124. point: number[],
  125. size: number[]
  126. ): Intersection[] {
  127. const tl = point
  128. const tr = vec.add(point, [size[0], 0])
  129. const br = vec.add(point, size)
  130. const bl = vec.add(point, [0, size[1]])
  131. const intersections: Intersection[] = []
  132. const topIntersection = intersectCircleLineSegment(c, r, tl, tr)
  133. const rightIntersection = intersectCircleLineSegment(c, r, tr, br)
  134. const bottomIntersection = intersectCircleLineSegment(c, r, bl, br)
  135. const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
  136. if (topIntersection.didIntersect) {
  137. intersections.push({ ...topIntersection, message: "top" })
  138. }
  139. if (rightIntersection.didIntersect) {
  140. intersections.push({ ...rightIntersection, message: "right" })
  141. }
  142. if (bottomIntersection.didIntersect) {
  143. intersections.push({ ...bottomIntersection, message: "bottom" })
  144. }
  145. if (leftIntersection.didIntersect) {
  146. intersections.push({ ...leftIntersection, message: "left" })
  147. }
  148. return intersections
  149. }
  150. export function intersectEllipseRectangle(
  151. c: number[],
  152. rx: number,
  153. ry: number,
  154. point: number[],
  155. size: number[],
  156. rotation = 0
  157. ): Intersection[] {
  158. const tl = point
  159. const tr = vec.add(point, [size[0], 0])
  160. const br = vec.add(point, size)
  161. const bl = vec.add(point, [0, size[1]])
  162. const intersections: Intersection[] = []
  163. const topIntersection = intersectEllipseLineSegment(
  164. c,
  165. rx,
  166. ry,
  167. tl,
  168. tr,
  169. rotation
  170. )
  171. const rightIntersection = intersectEllipseLineSegment(
  172. c,
  173. rx,
  174. ry,
  175. tr,
  176. br,
  177. rotation
  178. )
  179. const bottomIntersection = intersectEllipseLineSegment(
  180. c,
  181. rx,
  182. ry,
  183. bl,
  184. br,
  185. rotation
  186. )
  187. const leftIntersection = intersectEllipseLineSegment(
  188. c,
  189. rx,
  190. ry,
  191. tl,
  192. bl,
  193. rotation
  194. )
  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 intersectRectangleLineSegment(
  210. point: number[],
  211. size: number[],
  212. a1: number[],
  213. a2: number[]
  214. ) {
  215. const tl = point
  216. const tr = vec.add(point, [size[0], 0])
  217. const br = vec.add(point, size)
  218. const bl = vec.add(point, [0, size[1]])
  219. const intersections: Intersection[] = []
  220. const topIntersection = intersectLineSegments(a1, a2, tl, tr)
  221. const rightIntersection = intersectLineSegments(a1, a2, tr, br)
  222. const bottomIntersection = intersectLineSegments(a1, a2, bl, br)
  223. const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
  224. if (topIntersection.didIntersect) {
  225. intersections.push({ ...topIntersection, message: "top" })
  226. }
  227. if (rightIntersection.didIntersect) {
  228. intersections.push({ ...rightIntersection, message: "right" })
  229. }
  230. if (bottomIntersection.didIntersect) {
  231. intersections.push({ ...bottomIntersection, message: "bottom" })
  232. }
  233. if (leftIntersection.didIntersect) {
  234. intersections.push({ ...leftIntersection, message: "left" })
  235. }
  236. return intersections
  237. }
  238. /* -------------------------------------------------- */
  239. /* Shape vs. Bounds */
  240. /* -------------------------------------------------- */
  241. export function intersectCircleBounds(
  242. c: number[],
  243. r: number,
  244. bounds: Bounds
  245. ): Intersection[] {
  246. const { minX, minY, width, height } = bounds
  247. return intersectCircleRectangle(c, r, [minX, minY], [width, height])
  248. }
  249. export function intersectEllipseBounds(
  250. c: number[],
  251. rx: number,
  252. ry: number,
  253. bounds: Bounds,
  254. rotation = 0
  255. ): Intersection[] {
  256. const { minX, minY, width, height } = bounds
  257. return intersectEllipseRectangle(
  258. c,
  259. rx,
  260. ry,
  261. [minX, minY],
  262. [width, height],
  263. rotation
  264. )
  265. }
  266. export function intersectLineSegmentBounds(
  267. a1: number[],
  268. a2: number[],
  269. bounds: Bounds
  270. ) {
  271. const { minX, minY, width, height } = bounds
  272. return intersectRectangleLineSegment([minX, minY], [width, height], a1, a2)
  273. }
  274. export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
  275. const { minX, minY, width, height } = bounds
  276. const intersections: Intersection[] = []
  277. for (let i = 1; i < points.length; i++) {
  278. intersections.push(
  279. ...intersectRectangleLineSegment(
  280. [minX, minY],
  281. [width, height],
  282. points[i - 1],
  283. points[i]
  284. )
  285. )
  286. }
  287. return intersections
  288. }