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.

utils.ts 24KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. import Vector from "lib/code/vector"
  2. import React from "react"
  3. import { Data, Bounds, TransformEdge, TransformCorner } from "types"
  4. import * as svg from "./svg"
  5. import * as vec from "./vec"
  6. export function screenToWorld(point: number[], data: Data) {
  7. return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
  8. }
  9. /**
  10. * Get a bounding box that includes two bounding boxes.
  11. * @param a Bounding box
  12. * @param b Bounding box
  13. * @returns
  14. */
  15. export function getExpandedBounds(a: Bounds, b: Bounds) {
  16. const minX = Math.min(a.minX, b.minX),
  17. minY = Math.min(a.minY, b.minY),
  18. maxX = Math.max(a.maxX, b.maxX),
  19. maxY = Math.max(a.maxY, b.maxY),
  20. width = Math.abs(maxX - minX),
  21. height = Math.abs(maxY - minY)
  22. return { minX, minY, maxX, maxY, width, height }
  23. }
  24. /**
  25. * Get the common bounds of a group of bounds.
  26. * @returns
  27. */
  28. export function getCommonBounds(...b: Bounds[]) {
  29. if (b.length < 2) return b[0]
  30. let bounds = b[0]
  31. for (let i = 1; i < b.length; i++) {
  32. bounds = getExpandedBounds(bounds, b[i])
  33. }
  34. return bounds
  35. }
  36. // export function getBoundsFromPoints(a: number[], b: number[]) {
  37. // const minX = Math.min(a[0], b[0])
  38. // const maxX = Math.max(a[0], b[0])
  39. // const minY = Math.min(a[1], b[1])
  40. // const maxY = Math.max(a[1], b[1])
  41. // return {
  42. // minX,
  43. // maxX,
  44. // minY,
  45. // maxY,
  46. // width: maxX - minX,
  47. // height: maxY - minY,
  48. // }
  49. // }
  50. // A helper for getting tangents.
  51. export function getCircleTangentToPoint(
  52. A: number[],
  53. r0: number,
  54. P: number[],
  55. side: number
  56. ) {
  57. const B = vec.lrp(A, P, 0.5),
  58. r1 = vec.dist(A, B),
  59. delta = vec.sub(B, A),
  60. d = vec.len(delta)
  61. if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
  62. return
  63. }
  64. const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
  65. n = 1 / d,
  66. p = vec.add(A, vec.mul(delta, a * n)),
  67. h = Math.sqrt(r0 * r0 - a * a),
  68. k = vec.mul(vec.per(delta), h * n)
  69. return side === 0 ? vec.add(p, k) : vec.sub(p, k)
  70. }
  71. export function circleCircleIntersections(a: number[], b: number[]) {
  72. const R = a[2],
  73. r = b[2]
  74. let dx = b[0] - a[0],
  75. dy = b[1] - a[1]
  76. const d = Math.sqrt(dx * dx + dy * dy),
  77. x = (d * d - r * r + R * R) / (2 * d),
  78. y = Math.sqrt(R * R - x * x)
  79. dx /= d
  80. dy /= d
  81. return [
  82. [a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
  83. [a[0] + dx * x + dy * y, a[1] + dy * x - dx * y],
  84. ]
  85. }
  86. export function getClosestPointOnCircle(
  87. C: number[],
  88. r: number,
  89. P: number[],
  90. padding = 0
  91. ) {
  92. const v = vec.sub(C, P)
  93. return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r + padding))
  94. }
  95. export function projectPoint(p0: number[], a: number, d: number) {
  96. return [Math.cos(a) * d + p0[0], Math.sin(a) * d + p0[1]]
  97. }
  98. function shortAngleDist(a0: number, a1: number) {
  99. const max = Math.PI * 2
  100. const da = (a1 - a0) % max
  101. return ((2 * da) % max) - da
  102. }
  103. export function lerpAngles(a0: number, a1: number, t: number) {
  104. return a0 + shortAngleDist(a0, a1) * t
  105. }
  106. export function getBezierCurveSegments(points: number[][], tension = 0.4) {
  107. const len = points.length,
  108. cpoints: number[][] = [...points]
  109. if (len < 2) {
  110. throw Error("Curve must have at least two points.")
  111. }
  112. for (let i = 1; i < len - 1; i++) {
  113. const p0 = points[i - 1],
  114. p1 = points[i],
  115. p2 = points[i + 1]
  116. const pdx = p2[0] - p0[0],
  117. pdy = p2[1] - p0[1],
  118. pd = Math.hypot(pdx, pdy),
  119. nx = pdx / pd, // normalized x
  120. ny = pdy / pd, // normalized y
  121. dp = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), // Distance to previous
  122. dn = Math.hypot(p1[0] - p2[0], p1[1] - p2[1]) // Distance to next
  123. cpoints[i] = [
  124. // tangent start
  125. p1[0] - nx * dp * tension,
  126. p1[1] - ny * dp * tension,
  127. // tangent end
  128. p1[0] + nx * dn * tension,
  129. p1[1] + ny * dn * tension,
  130. // normal
  131. nx,
  132. ny,
  133. ]
  134. }
  135. // TODO: Reflect the nearest control points, not average them
  136. const d0 = Math.hypot(points[0][0] + cpoints[1][0])
  137. cpoints[0][2] = (points[0][0] + cpoints[1][0]) / 2
  138. cpoints[0][3] = (points[0][1] + cpoints[1][1]) / 2
  139. cpoints[0][4] = (cpoints[1][0] - points[0][0]) / d0
  140. cpoints[0][5] = (cpoints[1][1] - points[0][1]) / d0
  141. const d1 = Math.hypot(points[len - 1][1] + cpoints[len - 1][1])
  142. cpoints[len - 1][0] = (points[len - 1][0] + cpoints[len - 2][2]) / 2
  143. cpoints[len - 1][1] = (points[len - 1][1] + cpoints[len - 2][3]) / 2
  144. cpoints[len - 1][4] = (cpoints[len - 2][2] - points[len - 1][0]) / -d1
  145. cpoints[len - 1][5] = (cpoints[len - 2][3] - points[len - 1][1]) / -d1
  146. const results: {
  147. start: number[]
  148. tangentStart: number[]
  149. normalStart: number[]
  150. pressureStart: number
  151. end: number[]
  152. tangentEnd: number[]
  153. normalEnd: number[]
  154. pressureEnd: number
  155. }[] = []
  156. for (let i = 1; i < cpoints.length; i++) {
  157. results.push({
  158. start: points[i - 1].slice(0, 2),
  159. tangentStart: cpoints[i - 1].slice(2, 4),
  160. normalStart: cpoints[i - 1].slice(4, 6),
  161. pressureStart: 2 + ((i - 1) % 2 === 0 ? 1.5 : 0),
  162. end: points[i].slice(0, 2),
  163. tangentEnd: cpoints[i].slice(0, 2),
  164. normalEnd: cpoints[i].slice(4, 6),
  165. pressureEnd: 2 + (i % 2 === 0 ? 1.5 : 0),
  166. })
  167. }
  168. return results
  169. }
  170. export function cubicBezier(
  171. tx: number,
  172. x1: number,
  173. y1: number,
  174. x2: number,
  175. y2: number
  176. ) {
  177. // Inspired by Don Lancaster's two articles
  178. // http://www.tinaja.com/glib/cubemath.pdf
  179. // http://www.tinaja.com/text/bezmath.html
  180. // Set start and end point
  181. const x0 = 0,
  182. y0 = 0,
  183. x3 = 1,
  184. y3 = 1,
  185. // Convert the coordinates to equation space
  186. A = x3 - 3 * x2 + 3 * x1 - x0,
  187. B = 3 * x2 - 6 * x1 + 3 * x0,
  188. C = 3 * x1 - 3 * x0,
  189. D = x0,
  190. E = y3 - 3 * y2 + 3 * y1 - y0,
  191. F = 3 * y2 - 6 * y1 + 3 * y0,
  192. G = 3 * y1 - 3 * y0,
  193. H = y0,
  194. // Variables for the loop below
  195. iterations = 5
  196. let i: number,
  197. slope: number,
  198. x: number,
  199. t = tx
  200. // Loop through a few times to get a more accurate time value, according to the Newton-Raphson method
  201. // http://en.wikipedia.org/wiki/Newton's_method
  202. for (i = 0; i < iterations; i++) {
  203. // The curve's x equation for the current time value
  204. x = A * t * t * t + B * t * t + C * t + D
  205. // The slope we want is the inverse of the derivate of x
  206. slope = 1 / (3 * A * t * t + 2 * B * t + C)
  207. // Get the next estimated time value, which will be more accurate than the one before
  208. t -= (x - tx) * slope
  209. t = t > 1 ? 1 : t < 0 ? 0 : t
  210. }
  211. // Find the y value through the curve's y equation, with the now more accurate time value
  212. return Math.abs(E * t * t * t + F * t * t + G * t * H)
  213. }
  214. export function copyToClipboard(string: string) {
  215. let textarea: HTMLTextAreaElement
  216. let result: boolean
  217. try {
  218. navigator.clipboard.writeText(string)
  219. } catch (e) {
  220. try {
  221. textarea = document.createElement("textarea")
  222. textarea.setAttribute("position", "fixed")
  223. textarea.setAttribute("top", "0")
  224. textarea.setAttribute("readonly", "true")
  225. textarea.setAttribute("contenteditable", "true")
  226. textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set.
  227. textarea.value = string
  228. document.body.appendChild(textarea)
  229. textarea.focus()
  230. textarea.select()
  231. const range = document.createRange()
  232. range.selectNodeContents(textarea)
  233. const sel = window.getSelection()
  234. sel.removeAllRanges()
  235. sel.addRange(range)
  236. textarea.setSelectionRange(0, textarea.value.length)
  237. result = document.execCommand("copy")
  238. } catch (err) {
  239. result = null
  240. } finally {
  241. document.body.removeChild(textarea)
  242. }
  243. }
  244. return !!result
  245. }
  246. /**
  247. * Get a bezier curve data to for a spline that fits an array of points.
  248. * @param points An array of points formatted as [x, y]
  249. * @param k Tension
  250. * @returns An array of points as [cp1x, cp1y, cp2x, cp2y, px, py].
  251. */
  252. export function getSpline(pts: number[][], k = 0.5) {
  253. let p0: number[],
  254. [p1, p2, p3] = pts
  255. const results: number[][] = []
  256. for (let i = 1, len = pts.length; i < len; i++) {
  257. p0 = p1
  258. p1 = p2
  259. p2 = p3
  260. p3 = pts[i + 2] ? pts[i + 2] : p2
  261. results.push([
  262. p1[0] + ((p2[0] - p0[0]) / 6) * k,
  263. p1[1] + ((p2[1] - p0[1]) / 6) * k,
  264. p2[0] - ((p3[0] - p1[0]) / 6) * k,
  265. p2[1] - ((p3[1] - p1[1]) / 6) * k,
  266. pts[i][0],
  267. pts[i][1],
  268. ])
  269. }
  270. return results
  271. }
  272. export function getCurvePoints(
  273. pts: number[][],
  274. tension = 0.5,
  275. isClosed = false,
  276. numOfSegments = 3
  277. ) {
  278. const _pts = [...pts],
  279. len = pts.length,
  280. res: number[][] = [] // results
  281. let t1x: number, // tension vectors
  282. t2x: number,
  283. t1y: number,
  284. t2y: number,
  285. c1: number, // cardinal points
  286. c2: number,
  287. c3: number,
  288. c4: number,
  289. st: number,
  290. st2: number,
  291. st3: number
  292. // The algorithm require a previous and next point to the actual point array.
  293. // Check if we will draw closed or open curve.
  294. // If closed, copy end points to beginning and first points to end
  295. // If open, duplicate first points to befinning, end points to end
  296. if (isClosed) {
  297. _pts.unshift(_pts[len - 1])
  298. _pts.push(_pts[0])
  299. } else {
  300. //copy 1. point and insert at beginning
  301. _pts.unshift(_pts[0])
  302. _pts.push(_pts[len - 1])
  303. // _pts.push(_pts[len - 1])
  304. }
  305. // For each point, calculate a segment
  306. for (let i = 1; i < _pts.length - 2; i++) {
  307. // Calculate points along segment and add to results
  308. for (let t = 0; t <= numOfSegments; t++) {
  309. // Step
  310. st = t / numOfSegments
  311. st2 = Math.pow(st, 2)
  312. st3 = Math.pow(st, 3)
  313. // Cardinals
  314. c1 = 2 * st3 - 3 * st2 + 1
  315. c2 = -(2 * st3) + 3 * st2
  316. c3 = st3 - 2 * st2 + st
  317. c4 = st3 - st2
  318. // Tension
  319. t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
  320. t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
  321. t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
  322. t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
  323. // Control points
  324. res.push([
  325. c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
  326. c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
  327. ])
  328. }
  329. }
  330. res.push(pts[pts.length - 1])
  331. return res
  332. }
  333. export function angleDelta(a0: number, a1: number) {
  334. return shortAngleDist(a0, a1)
  335. }
  336. /**
  337. * Rotate a point around a center.
  338. * @param x The x-axis coordinate of the point.
  339. * @param y The y-axis coordinate of the point.
  340. * @param cx The x-axis coordinate of the point to rotate round.
  341. * @param cy The y-axis coordinate of the point to rotate round.
  342. * @param angle The distance (in radians) to rotate.
  343. */
  344. export function rotatePoint(A: number[], B: number[], angle: number) {
  345. const s = Math.sin(angle)
  346. const c = Math.cos(angle)
  347. const px = A[0] - B[0]
  348. const py = A[1] - B[1]
  349. const nx = px * c - py * s
  350. const ny = px * s + py * c
  351. return [nx + B[0], ny + B[1]]
  352. }
  353. export function degreesToRadians(d: number) {
  354. return (d * Math.PI) / 180
  355. }
  356. export function radiansToDegrees(r: number) {
  357. return (r * 180) / Math.PI
  358. }
  359. export function getArcLength(C: number[], r: number, A: number[], B: number[]) {
  360. const sweep = getSweep(C, A, B)
  361. return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
  362. }
  363. export function getArcDashOffset(
  364. C: number[],
  365. r: number,
  366. A: number[],
  367. B: number[],
  368. step: number
  369. ) {
  370. const del0 = getSweep(C, A, B)
  371. const len0 = getArcLength(C, r, A, B)
  372. const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
  373. return -off0 / 2 + step
  374. }
  375. export function getEllipseDashOffset(A: number[], step: number) {
  376. const c = 2 * Math.PI * A[2]
  377. return -c / 2 + -step
  378. }
  379. export function getSweep(C: number[], A: number[], B: number[]) {
  380. return angleDelta(vec.angle(C, A), vec.angle(C, B))
  381. }
  382. export function deepCompareArrays<T>(a: T[], b: T[]) {
  383. if (a?.length !== b?.length) return false
  384. return deepCompare(a, b)
  385. }
  386. export function deepCompare<T>(a: T, b: T) {
  387. return a === b || JSON.stringify(a) === JSON.stringify(b)
  388. }
  389. /**
  390. * Get outer tangents of two circles.
  391. * @param x0
  392. * @param y0
  393. * @param r0
  394. * @param x1
  395. * @param y1
  396. * @param r1
  397. * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
  398. */
  399. export function getOuterTangents(
  400. C0: number[],
  401. r0: number,
  402. C1: number[],
  403. r1: number
  404. ) {
  405. const a0 = vec.angle(C0, C1)
  406. const d = vec.dist(C0, C1)
  407. // Circles are overlapping, no tangents
  408. if (d < Math.abs(r1 - r0)) return
  409. const a1 = Math.acos((r0 - r1) / d),
  410. t0 = a0 + a1,
  411. t1 = a0 - a1
  412. return [
  413. [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
  414. [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
  415. [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
  416. [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
  417. ]
  418. }
  419. export function arrsIntersect<T, K>(
  420. a: T[],
  421. b: K[],
  422. fn?: (item: K) => T
  423. ): boolean
  424. export function arrsIntersect<T>(a: T[], b: T[]): boolean
  425. export function arrsIntersect<T>(
  426. a: T[],
  427. b: unknown[],
  428. fn?: (item: unknown) => T
  429. ) {
  430. return a.some((item) => b.includes(fn ? fn(item) : item))
  431. }
  432. // /**
  433. // * Will mutate an array to remove items.
  434. // * @param arr
  435. // * @param item
  436. // */
  437. // export function pull<T>(arr: T[], ...items: T[]) {
  438. // for (let item of items) {
  439. // arr.splice(arr.indexOf(item), 1)
  440. // }
  441. // return arr
  442. // }
  443. // /**
  444. // * Will mutate an array to remove items, based on a function
  445. // * @param arr
  446. // * @param fn
  447. // * @returns
  448. // */
  449. // export function pullWith<T>(arr: T[], fn: (item: T) => boolean) {
  450. // pull(arr, ...arr.filter((item) => fn(item)))
  451. // return arr
  452. // }
  453. // export function rectContainsRect(
  454. // x0: number,
  455. // y0: number,
  456. // x1: number,
  457. // y1: number,
  458. // box: { x: number; y: number; width: number; height: number }
  459. // ) {
  460. // return !(
  461. // x0 > box.x ||
  462. // x1 < box.x + box.width ||
  463. // y0 > box.y ||
  464. // y1 < box.y + box.height
  465. // )
  466. // }
  467. export function getTouchDisplay() {
  468. return (
  469. "ontouchstart" in window ||
  470. navigator.maxTouchPoints > 0 ||
  471. navigator.msMaxTouchPoints > 0
  472. )
  473. }
  474. const rounds = [1, 10, 100, 1000]
  475. export function round(n: number, p = 2) {
  476. return Math.floor(n * rounds[p]) / rounds[p]
  477. }
  478. /**
  479. * Linear interpolation betwen two numbers.
  480. * @param y1
  481. * @param y2
  482. * @param mu
  483. */
  484. export function lerp(y1: number, y2: number, mu: number) {
  485. mu = clamp(mu, 0, 1)
  486. return y1 * (1 - mu) + y2 * mu
  487. }
  488. /**
  489. * Modulate a value between two ranges.
  490. * @param value
  491. * @param rangeA from [low, high]
  492. * @param rangeB to [low, high]
  493. * @param clamp
  494. */
  495. export function modulate(
  496. value: number,
  497. rangeA: number[],
  498. rangeB: number[],
  499. clamp = false
  500. ) {
  501. const [fromLow, fromHigh] = rangeA
  502. const [v0, v1] = rangeB
  503. const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
  504. return clamp
  505. ? v0 < v1
  506. ? Math.max(Math.min(result, v1), v0)
  507. : Math.max(Math.min(result, v0), v1)
  508. : result
  509. }
  510. /**
  511. * Clamp a value into a range.
  512. * @param n
  513. * @param min
  514. */
  515. export function clamp(n: number, min: number): number
  516. export function clamp(n: number, min: number, max: number): number
  517. export function clamp(n: number, min: number, max?: number): number {
  518. return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
  519. }
  520. // CURVES
  521. // Mostly adapted from https://github.com/Pomax/bezierjs
  522. export function computePointOnCurve(t: number, points: number[][]) {
  523. // shortcuts
  524. if (t === 0) {
  525. return points[0]
  526. }
  527. const order = points.length - 1
  528. if (t === 1) {
  529. return points[order]
  530. }
  531. const mt = 1 - t
  532. let p = points // constant?
  533. if (order === 0) {
  534. return points[0]
  535. } // linear?
  536. if (order === 1) {
  537. return [mt * p[0][0] + t * p[1][0], mt * p[0][1] + t * p[1][1]]
  538. } // quadratic/cubic curve?
  539. if (order < 4) {
  540. const mt2 = mt * mt,
  541. t2 = t * t
  542. let a: number,
  543. b: number,
  544. c: number,
  545. d = 0
  546. if (order === 2) {
  547. p = [p[0], p[1], p[2], [0, 0]]
  548. a = mt2
  549. b = mt * t * 2
  550. c = t2
  551. } else if (order === 3) {
  552. a = mt2 * mt
  553. b = mt2 * t * 3
  554. c = mt * t2 * 3
  555. d = t * t2
  556. }
  557. return [
  558. a * p[0][0] + b * p[1][0] + c * p[2][0] + d * p[3][0],
  559. a * p[0][1] + b * p[1][1] + c * p[2][1] + d * p[3][1],
  560. ]
  561. } // higher order curves: use de Casteljau's computation
  562. }
  563. function distance2(p: DOMPoint, point: number[]) {
  564. const dx = p.x - point[0],
  565. dy = p.y - point[1]
  566. return dx * dx + dy * dy
  567. }
  568. /**
  569. * Find the closest point on a path to an off-path point.
  570. * @param pathNode
  571. * @param point
  572. * @returns
  573. */
  574. export function getClosestPointOnPath(
  575. pathNode: SVGPathElement,
  576. point: number[]
  577. ) {
  578. const pathLen = pathNode.getTotalLength()
  579. let p = 8,
  580. best: DOMPoint,
  581. bestLen: number,
  582. bestDist = Infinity,
  583. bl: number,
  584. al: number
  585. // linear scan for coarse approximation
  586. for (
  587. let scan: DOMPoint, scanLen = 0, scanDist: number;
  588. scanLen <= pathLen;
  589. scanLen += p
  590. ) {
  591. if (
  592. (scanDist = distance2(
  593. (scan = pathNode.getPointAtLength(scanLen)),
  594. point
  595. )) < bestDist
  596. ) {
  597. ;(best = scan), (bestLen = scanLen), (bestDist = scanDist)
  598. }
  599. }
  600. // binary search for precise estimate
  601. p /= 2
  602. while (p > 0.5) {
  603. let before: DOMPoint, after: DOMPoint, bd: number, ad: number
  604. if (
  605. (bl = bestLen - p) >= 0 &&
  606. (bd = distance2((before = pathNode.getPointAtLength(bl)), point)) <
  607. bestDist
  608. ) {
  609. ;(best = before), (bestLen = bl), (bestDist = bd)
  610. } else if (
  611. (al = bestLen + p) <= pathLen &&
  612. (ad = distance2((after = pathNode.getPointAtLength(al)), point)) <
  613. bestDist
  614. ) {
  615. ;(best = after), (bestLen = al), (bestDist = ad)
  616. } else {
  617. p /= 2
  618. }
  619. }
  620. return {
  621. point: [best.x, best.y],
  622. distance: bestDist,
  623. length: (bl + al) / 2,
  624. t: (bl + al) / 2 / pathLen,
  625. }
  626. }
  627. export function det(
  628. a: number,
  629. b: number,
  630. c: number,
  631. d: number,
  632. e: number,
  633. f: number,
  634. g: number,
  635. h: number,
  636. i: number
  637. ) {
  638. return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
  639. }
  640. /**
  641. * Get a circle from three points.
  642. * @param p0
  643. * @param p1
  644. * @param center
  645. * @returns
  646. */
  647. export function circleFromThreePoints(A: number[], B: number[], C: number[]) {
  648. const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
  649. const bx = -det(
  650. A[0] * A[0] + A[1] * A[1],
  651. A[1],
  652. 1,
  653. B[0] * B[0] + B[1] * B[1],
  654. B[1],
  655. 1,
  656. C[0] * C[0] + C[1] * C[1],
  657. C[1],
  658. 1
  659. )
  660. const by = det(
  661. A[0] * A[0] + A[1] * A[1],
  662. A[0],
  663. 1,
  664. B[0] * B[0] + B[1] * B[1],
  665. B[0],
  666. 1,
  667. C[0] * C[0] + C[1] * C[1],
  668. C[0],
  669. 1
  670. )
  671. const c = -det(
  672. A[0] * A[0] + A[1] * A[1],
  673. A[0],
  674. A[1],
  675. B[0] * B[0] + B[1] * B[1],
  676. B[0],
  677. B[1],
  678. C[0] * C[0] + C[1] * C[1],
  679. C[0],
  680. C[1]
  681. )
  682. return [
  683. -bx / (2 * a),
  684. -by / (2 * a),
  685. Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a)),
  686. ]
  687. }
  688. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  689. export function throttle<P extends any[], T extends (...args: P) => any>(
  690. fn: T,
  691. wait: number,
  692. preventDefault?: boolean
  693. ) {
  694. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  695. let inThrottle: boolean, lastFn: any, lastTime: number
  696. return function (...args: P) {
  697. if (preventDefault) args[0].preventDefault()
  698. // eslint-disable-next-line @typescript-eslint/no-this-alias
  699. const context = this
  700. if (!inThrottle) {
  701. fn.apply(context, args)
  702. lastTime = Date.now()
  703. inThrottle = true
  704. } else {
  705. clearTimeout(lastFn)
  706. lastFn = setTimeout(function () {
  707. if (Date.now() - lastTime >= wait) {
  708. fn.apply(context, args)
  709. lastTime = Date.now()
  710. }
  711. }, Math.max(wait - (Date.now() - lastTime), 0))
  712. }
  713. }
  714. }
  715. export function pointInRect(
  716. point: number[],
  717. minX: number,
  718. minY: number,
  719. maxX: number,
  720. maxY: number
  721. ) {
  722. return !(
  723. point[0] < minX ||
  724. point[0] > maxX ||
  725. point[1] < minY ||
  726. point[1] > maxY
  727. )
  728. }
  729. /**
  730. * Get the intersection of two rays, with origin points p0 and p1, and direction vectors n0 and n1.
  731. * @param p0 The origin point of the first ray
  732. * @param n0 The direction vector of the first ray
  733. * @param p1 The origin point of the second ray
  734. * @param n1 The direction vector of the second ray
  735. * @returns
  736. */
  737. export function getRayRayIntersection(
  738. p0: number[],
  739. n0: number[],
  740. p1: number[],
  741. n1: number[]
  742. ) {
  743. const p0e = vec.add(p0, n0),
  744. p1e = vec.add(p1, n1),
  745. m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
  746. m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
  747. b0 = p0[1] - m0 * p0[0],
  748. b1 = p1[1] - m1 * p1[0],
  749. x = (b1 - b0) / (m0 - m1),
  750. y = m0 * x + b0
  751. return [x, y]
  752. }
  753. export async function postJsonToEndpoint(
  754. endpoint: string,
  755. data: { [key: string]: unknown }
  756. ) {
  757. const d = await fetch(
  758. `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
  759. {
  760. method: "POST",
  761. headers: { "Content-Type": "application/json" },
  762. body: JSON.stringify(data),
  763. }
  764. )
  765. return await d.json()
  766. }
  767. export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
  768. const { shiftKey, ctrlKey, metaKey, altKey } = e
  769. return {
  770. key: e.key,
  771. shiftKey,
  772. ctrlKey,
  773. metaKey: isDarwin() ? metaKey : ctrlKey,
  774. altKey,
  775. }
  776. }
  777. export function isDarwin() {
  778. return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
  779. }
  780. export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
  781. return isDarwin() ? e.metaKey : e.ctrlKey
  782. }
  783. export function getTransformAnchor(
  784. type: TransformEdge | TransformCorner,
  785. isFlippedX: boolean,
  786. isFlippedY: boolean
  787. ) {
  788. let anchor: TransformCorner | TransformEdge = type
  789. // Change corner anchors if flipped
  790. switch (type) {
  791. case TransformCorner.TopLeft: {
  792. if (isFlippedX && isFlippedY) {
  793. anchor = TransformCorner.BottomRight
  794. } else if (isFlippedX) {
  795. anchor = TransformCorner.TopRight
  796. } else if (isFlippedY) {
  797. anchor = TransformCorner.BottomLeft
  798. }
  799. break
  800. }
  801. case TransformCorner.TopRight: {
  802. if (isFlippedX && isFlippedY) {
  803. anchor = TransformCorner.BottomLeft
  804. } else if (isFlippedX) {
  805. anchor = TransformCorner.TopLeft
  806. } else if (isFlippedY) {
  807. anchor = TransformCorner.BottomRight
  808. }
  809. break
  810. }
  811. case TransformCorner.BottomRight: {
  812. if (isFlippedX && isFlippedY) {
  813. anchor = TransformCorner.TopLeft
  814. } else if (isFlippedX) {
  815. anchor = TransformCorner.BottomLeft
  816. } else if (isFlippedY) {
  817. anchor = TransformCorner.TopRight
  818. }
  819. break
  820. }
  821. case TransformCorner.BottomLeft: {
  822. if (isFlippedX && isFlippedY) {
  823. anchor = TransformCorner.TopRight
  824. } else if (isFlippedX) {
  825. anchor = TransformCorner.BottomRight
  826. } else if (isFlippedY) {
  827. anchor = TransformCorner.TopLeft
  828. }
  829. break
  830. }
  831. }
  832. return anchor
  833. }
  834. export function vectorToPoint(point: number[] | Vector | undefined) {
  835. if (typeof point === "undefined") {
  836. return [0, 0]
  837. }
  838. if (point instanceof Vector) {
  839. return [point.x, point.y]
  840. }
  841. return point
  842. }
  843. export function getBoundsFromPoints(points: number[][]): Bounds {
  844. let minX = Infinity
  845. let minY = Infinity
  846. let maxX = -Infinity
  847. let maxY = -Infinity
  848. for (let [x, y] of points) {
  849. minX = Math.min(x, minX)
  850. minY = Math.min(y, minY)
  851. maxX = Math.max(x, maxX)
  852. maxY = Math.max(y, maxY)
  853. }
  854. return {
  855. minX,
  856. minY,
  857. maxX,
  858. maxY,
  859. width: maxX - minX,
  860. height: maxY - minY,
  861. }
  862. }
  863. /**
  864. * Move a bounding box without recalculating it.
  865. * @param bounds
  866. * @param delta
  867. * @returns
  868. */
  869. export function translateBounds(bounds: Bounds, delta: number[]) {
  870. return {
  871. minX: bounds.minX + delta[0],
  872. minY: bounds.minY + delta[1],
  873. maxX: bounds.maxX + delta[0],
  874. maxY: bounds.maxY + delta[1],
  875. width: bounds.width,
  876. height: bounds.height,
  877. }
  878. }
  879. export function rotateBounds(
  880. bounds: Bounds,
  881. center: number[],
  882. rotation: number
  883. ) {
  884. const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
  885. const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
  886. return {
  887. minX,
  888. minY,
  889. maxX,
  890. maxY,
  891. width: bounds.width,
  892. height: bounds.height,
  893. }
  894. }