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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662
  1. import Vector from 'lib/code/vector'
  2. import React from 'react'
  3. import {
  4. Data,
  5. Bounds,
  6. Edge,
  7. Corner,
  8. Shape,
  9. ShapeStyles,
  10. GroupShape,
  11. ShapeType,
  12. } from 'types'
  13. import * as vec from './vec'
  14. import _isMobile from 'ismobilejs'
  15. import { getShapeUtils } from 'lib/shape-utils'
  16. export function screenToWorld(point: number[], data: Data) {
  17. const camera = getCurrentCamera(data)
  18. return vec.sub(vec.div(point, camera.zoom), camera.point)
  19. }
  20. /**
  21. * Get a bounding box that includes two bounding boxes.
  22. * @param a Bounding box
  23. * @param b Bounding box
  24. * @returns
  25. */
  26. export function getExpandedBounds(a: Bounds, b: Bounds) {
  27. const minX = Math.min(a.minX, b.minX),
  28. minY = Math.min(a.minY, b.minY),
  29. maxX = Math.max(a.maxX, b.maxX),
  30. maxY = Math.max(a.maxY, b.maxY),
  31. width = Math.abs(maxX - minX),
  32. height = Math.abs(maxY - minY)
  33. return { minX, minY, maxX, maxY, width, height }
  34. }
  35. /**
  36. * Get the common bounds of a group of bounds.
  37. * @returns
  38. */
  39. export function getCommonBounds(...b: Bounds[]) {
  40. if (b.length < 2) return b[0]
  41. let bounds = b[0]
  42. for (let i = 1; i < b.length; i++) {
  43. bounds = getExpandedBounds(bounds, b[i])
  44. }
  45. return bounds
  46. }
  47. // export function getBoundsFromTwoPoints(a: number[], b: number[]) {
  48. // const minX = Math.min(a[0], b[0])
  49. // const maxX = Math.max(a[0], b[0])
  50. // const minY = Math.min(a[1], b[1])
  51. // const maxY = Math.max(a[1], b[1])
  52. // return {
  53. // minX,
  54. // maxX,
  55. // minY,
  56. // maxY,
  57. // width: maxX - minX,
  58. // height: maxY - minY,
  59. // }
  60. // }
  61. // A helper for getting tangents.
  62. export function getCircleTangentToPoint(
  63. A: number[],
  64. r0: number,
  65. P: number[],
  66. side: number
  67. ) {
  68. const B = vec.lrp(A, P, 0.5),
  69. r1 = vec.dist(A, B),
  70. delta = vec.sub(B, A),
  71. d = vec.len(delta)
  72. if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
  73. return
  74. }
  75. const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
  76. n = 1 / d,
  77. p = vec.add(A, vec.mul(delta, a * n)),
  78. h = Math.sqrt(r0 * r0 - a * a),
  79. k = vec.mul(vec.per(delta), h * n)
  80. return side === 0 ? vec.add(p, k) : vec.sub(p, k)
  81. }
  82. export function circleCircleIntersections(a: number[], b: number[]) {
  83. const R = a[2],
  84. r = b[2]
  85. let dx = b[0] - a[0],
  86. dy = b[1] - a[1]
  87. const d = Math.sqrt(dx * dx + dy * dy),
  88. x = (d * d - r * r + R * R) / (2 * d),
  89. y = Math.sqrt(R * R - x * x)
  90. dx /= d
  91. dy /= d
  92. return [
  93. [a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
  94. [a[0] + dx * x + dy * y, a[1] + dy * x - dx * y],
  95. ]
  96. }
  97. export function getClosestPointOnCircle(
  98. C: number[],
  99. r: number,
  100. P: number[],
  101. padding = 0
  102. ) {
  103. const v = vec.sub(C, P)
  104. return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r + padding))
  105. }
  106. export function projectPoint(p0: number[], a: number, d: number) {
  107. return [Math.cos(a) * d + p0[0], Math.sin(a) * d + p0[1]]
  108. }
  109. function shortAngleDist(a0: number, a1: number) {
  110. const max = Math.PI * 2
  111. const da = (a1 - a0) % max
  112. return ((2 * da) % max) - da
  113. }
  114. export function lerpAngles(a0: number, a1: number, t: number) {
  115. return a0 + shortAngleDist(a0, a1) * t
  116. }
  117. export function getBezierCurveSegments(points: number[][], tension = 0.4) {
  118. const len = points.length,
  119. cpoints: number[][] = [...points]
  120. if (len < 2) {
  121. throw Error('Curve must have at least two points.')
  122. }
  123. for (let i = 1; i < len - 1; i++) {
  124. const p0 = points[i - 1],
  125. p1 = points[i],
  126. p2 = points[i + 1]
  127. const pdx = p2[0] - p0[0],
  128. pdy = p2[1] - p0[1],
  129. pd = Math.hypot(pdx, pdy),
  130. nx = pdx / pd, // normalized x
  131. ny = pdy / pd, // normalized y
  132. dp = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), // Distance to previous
  133. dn = Math.hypot(p1[0] - p2[0], p1[1] - p2[1]) // Distance to next
  134. cpoints[i] = [
  135. // tangent start
  136. p1[0] - nx * dp * tension,
  137. p1[1] - ny * dp * tension,
  138. // tangent end
  139. p1[0] + nx * dn * tension,
  140. p1[1] + ny * dn * tension,
  141. // normal
  142. nx,
  143. ny,
  144. ]
  145. }
  146. // TODO: Reflect the nearest control points, not average them
  147. const d0 = Math.hypot(points[0][0] + cpoints[1][0])
  148. cpoints[0][2] = (points[0][0] + cpoints[1][0]) / 2
  149. cpoints[0][3] = (points[0][1] + cpoints[1][1]) / 2
  150. cpoints[0][4] = (cpoints[1][0] - points[0][0]) / d0
  151. cpoints[0][5] = (cpoints[1][1] - points[0][1]) / d0
  152. const d1 = Math.hypot(points[len - 1][1] + cpoints[len - 1][1])
  153. cpoints[len - 1][0] = (points[len - 1][0] + cpoints[len - 2][2]) / 2
  154. cpoints[len - 1][1] = (points[len - 1][1] + cpoints[len - 2][3]) / 2
  155. cpoints[len - 1][4] = (cpoints[len - 2][2] - points[len - 1][0]) / -d1
  156. cpoints[len - 1][5] = (cpoints[len - 2][3] - points[len - 1][1]) / -d1
  157. const results: {
  158. start: number[]
  159. tangentStart: number[]
  160. normalStart: number[]
  161. pressureStart: number
  162. end: number[]
  163. tangentEnd: number[]
  164. normalEnd: number[]
  165. pressureEnd: number
  166. }[] = []
  167. for (let i = 1; i < cpoints.length; i++) {
  168. results.push({
  169. start: points[i - 1].slice(0, 2),
  170. tangentStart: cpoints[i - 1].slice(2, 4),
  171. normalStart: cpoints[i - 1].slice(4, 6),
  172. pressureStart: 2 + ((i - 1) % 2 === 0 ? 1.5 : 0),
  173. end: points[i].slice(0, 2),
  174. tangentEnd: cpoints[i].slice(0, 2),
  175. normalEnd: cpoints[i].slice(4, 6),
  176. pressureEnd: 2 + (i % 2 === 0 ? 1.5 : 0),
  177. })
  178. }
  179. return results
  180. }
  181. export function cubicBezier(
  182. tx: number,
  183. x1: number,
  184. y1: number,
  185. x2: number,
  186. y2: number
  187. ) {
  188. // Inspired by Don Lancaster's two articles
  189. // http://www.tinaja.com/glib/cubemath.pdf
  190. // http://www.tinaja.com/text/bezmath.html
  191. // Set start and end point
  192. const x0 = 0,
  193. y0 = 0,
  194. x3 = 1,
  195. y3 = 1,
  196. // Convert the coordinates to equation space
  197. A = x3 - 3 * x2 + 3 * x1 - x0,
  198. B = 3 * x2 - 6 * x1 + 3 * x0,
  199. C = 3 * x1 - 3 * x0,
  200. D = x0,
  201. E = y3 - 3 * y2 + 3 * y1 - y0,
  202. F = 3 * y2 - 6 * y1 + 3 * y0,
  203. G = 3 * y1 - 3 * y0,
  204. H = y0,
  205. // Variables for the loop below
  206. iterations = 5
  207. let i: number,
  208. slope: number,
  209. x: number,
  210. t = tx
  211. // Loop through a few times to get a more accurate time value, according to the Newton-Raphson method
  212. // http://en.wikipedia.org/wiki/Newton's_method
  213. for (i = 0; i < iterations; i++) {
  214. // The curve's x equation for the current time value
  215. x = A * t * t * t + B * t * t + C * t + D
  216. // The slope we want is the inverse of the derivate of x
  217. slope = 1 / (3 * A * t * t + 2 * B * t + C)
  218. // Get the next estimated time value, which will be more accurate than the one before
  219. t -= (x - tx) * slope
  220. t = t > 1 ? 1 : t < 0 ? 0 : t
  221. }
  222. // Find the y value through the curve's y equation, with the now more accurate time value
  223. return Math.abs(E * t * t * t + F * t * t + G * t * H)
  224. }
  225. export function copyToClipboard(string: string) {
  226. let textarea: HTMLTextAreaElement
  227. let result: boolean
  228. try {
  229. navigator.clipboard.writeText(string)
  230. } catch (e) {
  231. try {
  232. textarea = document.createElement('textarea')
  233. textarea.setAttribute('position', 'fixed')
  234. textarea.setAttribute('top', '0')
  235. textarea.setAttribute('readonly', 'true')
  236. textarea.setAttribute('contenteditable', 'true')
  237. textarea.style.position = 'fixed' // prevent scroll from jumping to the bottom when focus is set.
  238. textarea.value = string
  239. document.body.appendChild(textarea)
  240. textarea.focus()
  241. textarea.select()
  242. const range = document.createRange()
  243. range.selectNodeContents(textarea)
  244. const sel = window.getSelection()
  245. sel.removeAllRanges()
  246. sel.addRange(range)
  247. textarea.setSelectionRange(0, textarea.value.length)
  248. result = document.execCommand('copy')
  249. } catch (err) {
  250. result = null
  251. } finally {
  252. document.body.removeChild(textarea)
  253. }
  254. }
  255. return !!result
  256. }
  257. /**
  258. * Get a bezier curve data to for a spline that fits an array of points.
  259. * @param points An array of points formatted as [x, y]
  260. * @param k Tension
  261. * @returns An array of points as [cp1x, cp1y, cp2x, cp2y, px, py].
  262. */
  263. export function getSpline(pts: number[][], k = 0.5) {
  264. let p0: number[],
  265. [p1, p2, p3] = pts
  266. const results: number[][] = []
  267. for (let i = 1, len = pts.length; i < len; i++) {
  268. p0 = p1
  269. p1 = p2
  270. p2 = p3
  271. p3 = pts[i + 2] ? pts[i + 2] : p2
  272. results.push([
  273. p1[0] + ((p2[0] - p0[0]) / 6) * k,
  274. p1[1] + ((p2[1] - p0[1]) / 6) * k,
  275. p2[0] - ((p3[0] - p1[0]) / 6) * k,
  276. p2[1] - ((p3[1] - p1[1]) / 6) * k,
  277. pts[i][0],
  278. pts[i][1],
  279. ])
  280. }
  281. return results
  282. }
  283. export function getCurvePoints(
  284. pts: number[][],
  285. tension = 0.5,
  286. isClosed = false,
  287. numOfSegments = 3
  288. ) {
  289. const _pts = [...pts],
  290. len = pts.length,
  291. res: number[][] = [] // results
  292. let t1x: number, // tension vectors
  293. t2x: number,
  294. t1y: number,
  295. t2y: number,
  296. c1: number, // cardinal points
  297. c2: number,
  298. c3: number,
  299. c4: number,
  300. st: number,
  301. st2: number,
  302. st3: number
  303. // The algorithm require a previous and next point to the actual point array.
  304. // Check if we will draw closed or open curve.
  305. // If closed, copy end points to beginning and first points to end
  306. // If open, duplicate first points to befinning, end points to end
  307. if (isClosed) {
  308. _pts.unshift(_pts[len - 1])
  309. _pts.push(_pts[0])
  310. } else {
  311. //copy 1. point and insert at beginning
  312. _pts.unshift(_pts[0])
  313. _pts.push(_pts[len - 1])
  314. // _pts.push(_pts[len - 1])
  315. }
  316. // For each point, calculate a segment
  317. for (let i = 1; i < _pts.length - 2; i++) {
  318. // Calculate points along segment and add to results
  319. for (let t = 0; t <= numOfSegments; t++) {
  320. // Step
  321. st = t / numOfSegments
  322. st2 = Math.pow(st, 2)
  323. st3 = Math.pow(st, 3)
  324. // Cardinals
  325. c1 = 2 * st3 - 3 * st2 + 1
  326. c2 = -(2 * st3) + 3 * st2
  327. c3 = st3 - 2 * st2 + st
  328. c4 = st3 - st2
  329. // Tension
  330. t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
  331. t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
  332. t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
  333. t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
  334. // Control points
  335. res.push([
  336. c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
  337. c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
  338. ])
  339. }
  340. }
  341. res.push(pts[pts.length - 1])
  342. return res
  343. }
  344. export function angleDelta(a0: number, a1: number) {
  345. return shortAngleDist(a0, a1)
  346. }
  347. /**
  348. * Rotate a point around a center.
  349. * @param x The x-axis coordinate of the point.
  350. * @param y The y-axis coordinate of the point.
  351. * @param cx The x-axis coordinate of the point to rotate round.
  352. * @param cy The y-axis coordinate of the point to rotate round.
  353. * @param angle The distance (in radians) to rotate.
  354. */
  355. export function rotatePoint(A: number[], B: number[], angle: number) {
  356. const s = Math.sin(angle)
  357. const c = Math.cos(angle)
  358. const px = A[0] - B[0]
  359. const py = A[1] - B[1]
  360. const nx = px * c - py * s
  361. const ny = px * s + py * c
  362. return [nx + B[0], ny + B[1]]
  363. }
  364. export function degreesToRadians(d: number) {
  365. return (d * Math.PI) / 180
  366. }
  367. export function radiansToDegrees(r: number) {
  368. return (r * 180) / Math.PI
  369. }
  370. export function getArcLength(C: number[], r: number, A: number[], B: number[]) {
  371. const sweep = getSweep(C, A, B)
  372. return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
  373. }
  374. export function getArcDashOffset(
  375. C: number[],
  376. r: number,
  377. A: number[],
  378. B: number[],
  379. step: number
  380. ) {
  381. const del0 = getSweep(C, A, B)
  382. const len0 = getArcLength(C, r, A, B)
  383. const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
  384. return -off0 / 2 + step
  385. }
  386. export function getEllipseDashOffset(A: number[], step: number) {
  387. const c = 2 * Math.PI * A[2]
  388. return -c / 2 + -step
  389. }
  390. export function getSweep(C: number[], A: number[], B: number[]) {
  391. return angleDelta(vec.angle(C, A), vec.angle(C, B))
  392. }
  393. export function deepCompareArrays<T>(a: T[], b: T[]) {
  394. if (a?.length !== b?.length) return false
  395. return deepCompare(a, b)
  396. }
  397. export function deepCompare<T>(a: T, b: T) {
  398. return a === b || JSON.stringify(a) === JSON.stringify(b)
  399. }
  400. /**
  401. * Get outer tangents of two circles.
  402. * @param x0
  403. * @param y0
  404. * @param r0
  405. * @param x1
  406. * @param y1
  407. * @param r1
  408. * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
  409. */
  410. export function getOuterTangents(
  411. C0: number[],
  412. r0: number,
  413. C1: number[],
  414. r1: number
  415. ) {
  416. const a0 = vec.angle(C0, C1)
  417. const d = vec.dist(C0, C1)
  418. // Circles are overlapping, no tangents
  419. if (d < Math.abs(r1 - r0)) return
  420. const a1 = Math.acos((r0 - r1) / d),
  421. t0 = a0 + a1,
  422. t1 = a0 - a1
  423. return [
  424. [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
  425. [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
  426. [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
  427. [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
  428. ]
  429. }
  430. export function arrsIntersect<T, K>(
  431. a: T[],
  432. b: K[],
  433. fn?: (item: K) => T
  434. ): boolean
  435. export function arrsIntersect<T>(a: T[], b: T[]): boolean
  436. export function arrsIntersect<T>(
  437. a: T[],
  438. b: unknown[],
  439. fn?: (item: unknown) => T
  440. ) {
  441. return a.some((item) => b.includes(fn ? fn(item) : item))
  442. }
  443. // /**
  444. // * Will mutate an array to remove items.
  445. // * @param arr
  446. // * @param item
  447. // */
  448. // export function pull<T>(arr: T[], ...items: T[]) {
  449. // for (let item of items) {
  450. // arr.splice(arr.indexOf(item), 1)
  451. // }
  452. // return arr
  453. // }
  454. // /**
  455. // * Will mutate an array to remove items, based on a function
  456. // * @param arr
  457. // * @param fn
  458. // * @returns
  459. // */
  460. // export function pullWith<T>(arr: T[], fn: (item: T) => boolean) {
  461. // pull(arr, ...arr.filter((item) => fn(item)))
  462. // return arr
  463. // }
  464. // export function rectContainsRect(
  465. // x0: number,
  466. // y0: number,
  467. // x1: number,
  468. // y1: number,
  469. // box: { x: number; y: number; width: number; height: number }
  470. // ) {
  471. // return !(
  472. // x0 > box.x ||
  473. // x1 < box.x + box.width ||
  474. // y0 > box.y ||
  475. // y1 < box.y + box.height
  476. // )
  477. // }
  478. export function getTouchDisplay() {
  479. return (
  480. 'ontouchstart' in window ||
  481. navigator.maxTouchPoints > 0 ||
  482. navigator.msMaxTouchPoints > 0
  483. )
  484. }
  485. const rounds = [1, 10, 100, 1000]
  486. export function round(n: number, p = 2) {
  487. return Math.floor(n * rounds[p]) / rounds[p]
  488. }
  489. /**
  490. * Linear interpolation betwen two numbers.
  491. * @param y1
  492. * @param y2
  493. * @param mu
  494. */
  495. export function lerp(y1: number, y2: number, mu: number) {
  496. mu = clamp(mu, 0, 1)
  497. return y1 * (1 - mu) + y2 * mu
  498. }
  499. /**
  500. * Modulate a value between two ranges.
  501. * @param value
  502. * @param rangeA from [low, high]
  503. * @param rangeB to [low, high]
  504. * @param clamp
  505. */
  506. export function modulate(
  507. value: number,
  508. rangeA: number[],
  509. rangeB: number[],
  510. clamp = false
  511. ) {
  512. const [fromLow, fromHigh] = rangeA
  513. const [v0, v1] = rangeB
  514. const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
  515. return clamp
  516. ? v0 < v1
  517. ? Math.max(Math.min(result, v1), v0)
  518. : Math.max(Math.min(result, v0), v1)
  519. : result
  520. }
  521. /**
  522. * Clamp a value into a range.
  523. * @param n
  524. * @param min
  525. */
  526. export function clamp(n: number, min: number): number
  527. export function clamp(n: number, min: number, max: number): number
  528. export function clamp(n: number, min: number, max?: number): number {
  529. return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
  530. }
  531. // CURVES
  532. // Mostly adapted from https://github.com/Pomax/bezierjs
  533. export function computePointOnCurve(t: number, points: number[][]) {
  534. // shortcuts
  535. if (t === 0) {
  536. return points[0]
  537. }
  538. const order = points.length - 1
  539. if (t === 1) {
  540. return points[order]
  541. }
  542. const mt = 1 - t
  543. let p = points // constant?
  544. if (order === 0) {
  545. return points[0]
  546. } // linear?
  547. if (order === 1) {
  548. return [mt * p[0][0] + t * p[1][0], mt * p[0][1] + t * p[1][1]]
  549. } // quadratic/cubic curve?
  550. if (order < 4) {
  551. const mt2 = mt * mt,
  552. t2 = t * t
  553. let a: number,
  554. b: number,
  555. c: number,
  556. d = 0
  557. if (order === 2) {
  558. p = [p[0], p[1], p[2], [0, 0]]
  559. a = mt2
  560. b = mt * t * 2
  561. c = t2
  562. } else if (order === 3) {
  563. a = mt2 * mt
  564. b = mt2 * t * 3
  565. c = mt * t2 * 3
  566. d = t * t2
  567. }
  568. return [
  569. a * p[0][0] + b * p[1][0] + c * p[2][0] + d * p[3][0],
  570. a * p[0][1] + b * p[1][1] + c * p[2][1] + d * p[3][1],
  571. ]
  572. } // higher order curves: use de Casteljau's computation
  573. }
  574. function distance2(p: DOMPoint, point: number[]) {
  575. const dx = p.x - point[0],
  576. dy = p.y - point[1]
  577. return dx * dx + dy * dy
  578. }
  579. /**
  580. * Find the closest point on a path to an off-path point.
  581. * @param pathNode
  582. * @param point
  583. * @returns
  584. */
  585. export function getClosestPointOnPath(
  586. pathNode: SVGPathElement,
  587. point: number[]
  588. ) {
  589. const pathLen = pathNode.getTotalLength()
  590. let p = 8,
  591. best: DOMPoint,
  592. bestLen: number,
  593. bestDist = Infinity,
  594. bl: number,
  595. al: number
  596. // linear scan for coarse approximation
  597. for (
  598. let scan: DOMPoint, scanLen = 0, scanDist: number;
  599. scanLen <= pathLen;
  600. scanLen += p
  601. ) {
  602. if (
  603. (scanDist = distance2(
  604. (scan = pathNode.getPointAtLength(scanLen)),
  605. point
  606. )) < bestDist
  607. ) {
  608. ;(best = scan), (bestLen = scanLen), (bestDist = scanDist)
  609. }
  610. }
  611. // binary search for precise estimate
  612. p /= 2
  613. while (p > 0.5) {
  614. let before: DOMPoint, after: DOMPoint, bd: number, ad: number
  615. if (
  616. (bl = bestLen - p) >= 0 &&
  617. (bd = distance2((before = pathNode.getPointAtLength(bl)), point)) <
  618. bestDist
  619. ) {
  620. ;(best = before), (bestLen = bl), (bestDist = bd)
  621. } else if (
  622. (al = bestLen + p) <= pathLen &&
  623. (ad = distance2((after = pathNode.getPointAtLength(al)), point)) <
  624. bestDist
  625. ) {
  626. ;(best = after), (bestLen = al), (bestDist = ad)
  627. } else {
  628. p /= 2
  629. }
  630. }
  631. return {
  632. point: [best.x, best.y],
  633. distance: bestDist,
  634. length: (bl + al) / 2,
  635. t: (bl + al) / 2 / pathLen,
  636. }
  637. }
  638. export function det(
  639. a: number,
  640. b: number,
  641. c: number,
  642. d: number,
  643. e: number,
  644. f: number,
  645. g: number,
  646. h: number,
  647. i: number
  648. ) {
  649. return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
  650. }
  651. /**
  652. * Get a circle from three points.
  653. * @param p0
  654. * @param p1
  655. * @param center
  656. * @returns [x, y, r]
  657. */
  658. export function circleFromThreePoints(A: number[], B: number[], C: number[]) {
  659. const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
  660. const bx = -det(
  661. A[0] * A[0] + A[1] * A[1],
  662. A[1],
  663. 1,
  664. B[0] * B[0] + B[1] * B[1],
  665. B[1],
  666. 1,
  667. C[0] * C[0] + C[1] * C[1],
  668. C[1],
  669. 1
  670. )
  671. const by = det(
  672. A[0] * A[0] + A[1] * A[1],
  673. A[0],
  674. 1,
  675. B[0] * B[0] + B[1] * B[1],
  676. B[0],
  677. 1,
  678. C[0] * C[0] + C[1] * C[1],
  679. C[0],
  680. 1
  681. )
  682. const c = -det(
  683. A[0] * A[0] + A[1] * A[1],
  684. A[0],
  685. A[1],
  686. B[0] * B[0] + B[1] * B[1],
  687. B[0],
  688. B[1],
  689. C[0] * C[0] + C[1] * C[1],
  690. C[0],
  691. C[1]
  692. )
  693. const x = -bx / (2 * a)
  694. const y = -by / (2 * a)
  695. const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
  696. return [x, y, r]
  697. }
  698. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  699. export function throttle<P extends any[], T extends (...args: P) => any>(
  700. fn: T,
  701. wait: number,
  702. preventDefault?: boolean
  703. ) {
  704. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  705. let inThrottle: boolean, lastFn: any, lastTime: number
  706. return function (...args: P) {
  707. if (preventDefault) args[0].preventDefault()
  708. // eslint-disable-next-line @typescript-eslint/no-this-alias
  709. const context = this
  710. if (!inThrottle) {
  711. fn.apply(context, args)
  712. lastTime = Date.now()
  713. inThrottle = true
  714. } else {
  715. clearTimeout(lastFn)
  716. lastFn = setTimeout(function () {
  717. if (Date.now() - lastTime >= wait) {
  718. fn.apply(context, args)
  719. lastTime = Date.now()
  720. }
  721. }, Math.max(wait - (Date.now() - lastTime), 0))
  722. }
  723. }
  724. }
  725. export function pointInRect(
  726. point: number[],
  727. minX: number,
  728. minY: number,
  729. maxX: number,
  730. maxY: number
  731. ) {
  732. return !(
  733. point[0] < minX ||
  734. point[0] > maxX ||
  735. point[1] < minY ||
  736. point[1] > maxY
  737. )
  738. }
  739. /**
  740. * Get the intersection of two rays, with origin points p0 and p1, and direction vectors n0 and n1.
  741. * @param p0 The origin point of the first ray
  742. * @param n0 The direction vector of the first ray
  743. * @param p1 The origin point of the second ray
  744. * @param n1 The direction vector of the second ray
  745. * @returns
  746. */
  747. export function getRayRayIntersection(
  748. p0: number[],
  749. n0: number[],
  750. p1: number[],
  751. n1: number[]
  752. ) {
  753. const p0e = vec.add(p0, n0),
  754. p1e = vec.add(p1, n1),
  755. m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
  756. m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
  757. b0 = p0[1] - m0 * p0[0],
  758. b1 = p1[1] - m1 * p1[0],
  759. x = (b1 - b0) / (m0 - m1),
  760. y = m0 * x + b0
  761. return [x, y]
  762. }
  763. export async function postJsonToEndpoint(
  764. endpoint: string,
  765. data: { [key: string]: unknown }
  766. ) {
  767. const d = await fetch(
  768. `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
  769. {
  770. method: 'POST',
  771. headers: { 'Content-Type': 'application/json' },
  772. body: JSON.stringify(data),
  773. }
  774. )
  775. return await d.json()
  776. }
  777. export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
  778. const { shiftKey, ctrlKey, metaKey, altKey } = e
  779. return {
  780. key: e.key,
  781. shiftKey,
  782. ctrlKey,
  783. metaKey: isDarwin() ? metaKey : ctrlKey,
  784. altKey,
  785. }
  786. }
  787. export function isDarwin() {
  788. return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
  789. }
  790. export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
  791. return isDarwin() ? e.metaKey : e.ctrlKey
  792. }
  793. export function getTransformAnchor(
  794. type: Edge | Corner,
  795. isFlippedX: boolean,
  796. isFlippedY: boolean
  797. ) {
  798. let anchor: Corner | Edge = type
  799. // Change corner anchors if flipped
  800. switch (type) {
  801. case Corner.TopLeft: {
  802. if (isFlippedX && isFlippedY) {
  803. anchor = Corner.BottomRight
  804. } else if (isFlippedX) {
  805. anchor = Corner.TopRight
  806. } else if (isFlippedY) {
  807. anchor = Corner.BottomLeft
  808. } else {
  809. anchor = Corner.BottomRight
  810. }
  811. break
  812. }
  813. case Corner.TopRight: {
  814. if (isFlippedX && isFlippedY) {
  815. anchor = Corner.BottomLeft
  816. } else if (isFlippedX) {
  817. anchor = Corner.TopLeft
  818. } else if (isFlippedY) {
  819. anchor = Corner.BottomRight
  820. } else {
  821. anchor = Corner.BottomLeft
  822. }
  823. break
  824. }
  825. case Corner.BottomRight: {
  826. if (isFlippedX && isFlippedY) {
  827. anchor = Corner.TopLeft
  828. } else if (isFlippedX) {
  829. anchor = Corner.BottomLeft
  830. } else if (isFlippedY) {
  831. anchor = Corner.TopRight
  832. } else {
  833. anchor = Corner.TopLeft
  834. }
  835. break
  836. }
  837. case Corner.BottomLeft: {
  838. if (isFlippedX && isFlippedY) {
  839. anchor = Corner.TopRight
  840. } else if (isFlippedX) {
  841. anchor = Corner.BottomRight
  842. } else if (isFlippedY) {
  843. anchor = Corner.TopLeft
  844. } else {
  845. anchor = Corner.TopRight
  846. }
  847. break
  848. }
  849. }
  850. return anchor
  851. }
  852. export function vectorToPoint(point: number[] | Vector | undefined) {
  853. if (typeof point === 'undefined') {
  854. return [0, 0]
  855. }
  856. if (point instanceof Vector) {
  857. return [point.x, point.y]
  858. }
  859. return point
  860. }
  861. export function getBoundsFromPoints(points: number[][], rotation = 0): Bounds {
  862. let minX = Infinity
  863. let minY = Infinity
  864. let maxX = -Infinity
  865. let maxY = -Infinity
  866. if (points.length < 2) {
  867. minX = 0
  868. minY = 0
  869. maxX = 1
  870. maxY = 1
  871. } else {
  872. for (let [x, y] of points) {
  873. minX = Math.min(x, minX)
  874. minY = Math.min(y, minY)
  875. maxX = Math.max(x, maxX)
  876. maxY = Math.max(y, maxY)
  877. }
  878. }
  879. if (rotation !== 0) {
  880. return getBoundsFromPoints(
  881. points.map((pt) =>
  882. vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
  883. )
  884. )
  885. }
  886. return {
  887. minX,
  888. minY,
  889. maxX,
  890. maxY,
  891. width: Math.max(1, maxX - minX),
  892. height: Math.max(1, maxY - minY),
  893. }
  894. }
  895. /**
  896. * Move a bounding box without recalculating it.
  897. * @param bounds
  898. * @param delta
  899. * @returns
  900. */
  901. export function translateBounds(bounds: Bounds, delta: number[]) {
  902. return {
  903. minX: bounds.minX + delta[0],
  904. minY: bounds.minY + delta[1],
  905. maxX: bounds.maxX + delta[0],
  906. maxY: bounds.maxY + delta[1],
  907. width: bounds.width,
  908. height: bounds.height,
  909. }
  910. }
  911. export function rotateBounds(
  912. bounds: Bounds,
  913. center: number[],
  914. rotation: number
  915. ) {
  916. const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
  917. const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
  918. return {
  919. minX,
  920. minY,
  921. maxX,
  922. maxY,
  923. width: bounds.width,
  924. height: bounds.height,
  925. }
  926. }
  927. export function getRotatedSize(size: number[], rotation: number) {
  928. const center = vec.div(size, 2)
  929. const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
  930. vec.rotWith(point, center, rotation)
  931. )
  932. const bounds = getBoundsFromPoints(points)
  933. return [bounds.width, bounds.height]
  934. }
  935. export function getRotatedCorners(b: Bounds, rotation: number) {
  936. const center = [b.minX + b.width / 2, b.minY + b.height / 2]
  937. return [
  938. [b.minX, b.minY],
  939. [b.maxX, b.minY],
  940. [b.maxX, b.maxY],
  941. [b.minX, b.maxY],
  942. ].map((point) => vec.rotWith(point, center, rotation))
  943. }
  944. export function getTransformedBoundingBox(
  945. bounds: Bounds,
  946. handle: Corner | Edge | 'center',
  947. delta: number[],
  948. rotation = 0,
  949. isAspectRatioLocked = false
  950. ) {
  951. // Create top left and bottom right corners.
  952. let [ax0, ay0] = [bounds.minX, bounds.minY]
  953. let [ax1, ay1] = [bounds.maxX, bounds.maxY]
  954. // Create a second set of corners for the new box.
  955. let [bx0, by0] = [bounds.minX, bounds.minY]
  956. let [bx1, by1] = [bounds.maxX, bounds.maxY]
  957. // If the drag is on the center, just translate the bounds.
  958. if (handle === 'center') {
  959. return {
  960. minX: bx0 + delta[0],
  961. minY: by0 + delta[1],
  962. maxX: bx1 + delta[0],
  963. maxY: by1 + delta[1],
  964. width: bx1 - bx0,
  965. height: by1 - by0,
  966. scaleX: 1,
  967. scaleY: 1,
  968. }
  969. }
  970. // Counter rotate the delta. This lets us make changes as if
  971. // the (possibly rotated) boxes were axis aligned.
  972. let [dx, dy] = vec.rot(delta, -rotation)
  973. /*
  974. 1. Delta
  975. Use the delta to adjust the new box by changing its corners.
  976. The dragging handle (corner or edge) will determine which
  977. corners should change.
  978. */
  979. switch (handle) {
  980. case Edge.Top:
  981. case Corner.TopLeft:
  982. case Corner.TopRight: {
  983. by0 += dy
  984. break
  985. }
  986. case Edge.Bottom:
  987. case Corner.BottomLeft:
  988. case Corner.BottomRight: {
  989. by1 += dy
  990. break
  991. }
  992. }
  993. switch (handle) {
  994. case Edge.Left:
  995. case Corner.TopLeft:
  996. case Corner.BottomLeft: {
  997. bx0 += dx
  998. break
  999. }
  1000. case Edge.Right:
  1001. case Corner.TopRight:
  1002. case Corner.BottomRight: {
  1003. bx1 += dx
  1004. break
  1005. }
  1006. }
  1007. const aw = ax1 - ax0
  1008. const ah = ay1 - ay0
  1009. const scaleX = (bx1 - bx0) / aw
  1010. const scaleY = (by1 - by0) / ah
  1011. const flipX = scaleX < 0
  1012. const flipY = scaleY < 0
  1013. const bw = Math.abs(bx1 - bx0)
  1014. const bh = Math.abs(by1 - by0)
  1015. /*
  1016. 2. Aspect ratio
  1017. If the aspect ratio is locked, adjust the corners so that the
  1018. new box's aspect ratio matches the original aspect ratio.
  1019. */
  1020. if (isAspectRatioLocked) {
  1021. const ar = aw / ah
  1022. const isTall = ar < bw / bh
  1023. const tw = bw * (scaleY < 0 ? 1 : -1) * (1 / ar)
  1024. const th = bh * (scaleX < 0 ? 1 : -1) * ar
  1025. switch (handle) {
  1026. case Corner.TopLeft: {
  1027. if (isTall) by0 = by1 + tw
  1028. else bx0 = bx1 + th
  1029. break
  1030. }
  1031. case Corner.TopRight: {
  1032. if (isTall) by0 = by1 + tw
  1033. else bx1 = bx0 - th
  1034. break
  1035. }
  1036. case Corner.BottomRight: {
  1037. if (isTall) by1 = by0 - tw
  1038. else bx1 = bx0 - th
  1039. break
  1040. }
  1041. case Corner.BottomLeft: {
  1042. if (isTall) by1 = by0 - tw
  1043. else bx0 = bx1 + th
  1044. break
  1045. }
  1046. case Edge.Bottom:
  1047. case Edge.Top: {
  1048. const m = (bx0 + bx1) / 2
  1049. const w = bh * ar
  1050. bx0 = m - w / 2
  1051. bx1 = m + w / 2
  1052. break
  1053. }
  1054. case Edge.Left:
  1055. case Edge.Right: {
  1056. const m = (by0 + by1) / 2
  1057. const h = bw / ar
  1058. by0 = m - h / 2
  1059. by1 = m + h / 2
  1060. break
  1061. }
  1062. }
  1063. }
  1064. /*
  1065. 3. Rotation
  1066. If the bounds are rotated, get a vector from the rotated anchor
  1067. corner in the inital bounds to the rotated anchor corner in the
  1068. result's bounds. Subtract this vector from the result's corners,
  1069. so that the two anchor points (initial and result) will be equal.
  1070. */
  1071. if (rotation % (Math.PI * 2) !== 0) {
  1072. let cv = [0, 0]
  1073. const c0 = vec.med([ax0, ay0], [ax1, ay1])
  1074. const c1 = vec.med([bx0, by0], [bx1, by1])
  1075. switch (handle) {
  1076. case Corner.TopLeft: {
  1077. cv = vec.sub(
  1078. vec.rotWith([bx1, by1], c1, rotation),
  1079. vec.rotWith([ax1, ay1], c0, rotation)
  1080. )
  1081. break
  1082. }
  1083. case Corner.TopRight: {
  1084. cv = vec.sub(
  1085. vec.rotWith([bx0, by1], c1, rotation),
  1086. vec.rotWith([ax0, ay1], c0, rotation)
  1087. )
  1088. break
  1089. }
  1090. case Corner.BottomRight: {
  1091. cv = vec.sub(
  1092. vec.rotWith([bx0, by0], c1, rotation),
  1093. vec.rotWith([ax0, ay0], c0, rotation)
  1094. )
  1095. break
  1096. }
  1097. case Corner.BottomLeft: {
  1098. cv = vec.sub(
  1099. vec.rotWith([bx1, by0], c1, rotation),
  1100. vec.rotWith([ax1, ay0], c0, rotation)
  1101. )
  1102. break
  1103. }
  1104. case Edge.Top: {
  1105. cv = vec.sub(
  1106. vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
  1107. vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
  1108. )
  1109. break
  1110. }
  1111. case Edge.Left: {
  1112. cv = vec.sub(
  1113. vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
  1114. vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
  1115. )
  1116. break
  1117. }
  1118. case Edge.Bottom: {
  1119. cv = vec.sub(
  1120. vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
  1121. vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
  1122. )
  1123. break
  1124. }
  1125. case Edge.Right: {
  1126. cv = vec.sub(
  1127. vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
  1128. vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
  1129. )
  1130. break
  1131. }
  1132. }
  1133. ;[bx0, by0] = vec.sub([bx0, by0], cv)
  1134. ;[bx1, by1] = vec.sub([bx1, by1], cv)
  1135. }
  1136. /*
  1137. 4. Flips
  1138. If the axes are flipped (e.g. if the right edge has been dragged
  1139. left past the initial left edge) then swap points on that axis.
  1140. */
  1141. if (bx1 < bx0) {
  1142. ;[bx1, bx0] = [bx0, bx1]
  1143. }
  1144. if (by1 < by0) {
  1145. ;[by1, by0] = [by0, by1]
  1146. }
  1147. return {
  1148. minX: bx0,
  1149. minY: by0,
  1150. maxX: bx1,
  1151. maxY: by1,
  1152. width: bx1 - bx0,
  1153. height: by1 - by0,
  1154. scaleX: ((bx1 - bx0) / (ax1 - ax0 || 1)) * (flipX ? -1 : 1),
  1155. scaleY: ((by1 - by0) / (ay1 - ay0 || 1)) * (flipY ? -1 : 1),
  1156. }
  1157. }
  1158. export function getRelativeTransformedBoundingBox(
  1159. bounds: Bounds,
  1160. initialBounds: Bounds,
  1161. initialShapeBounds: Bounds,
  1162. isFlippedX: boolean,
  1163. isFlippedY: boolean
  1164. ) {
  1165. const nx =
  1166. (isFlippedX
  1167. ? initialBounds.maxX - initialShapeBounds.maxX
  1168. : initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
  1169. const ny =
  1170. (isFlippedY
  1171. ? initialBounds.maxY - initialShapeBounds.maxY
  1172. : initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
  1173. const nw = initialShapeBounds.width / initialBounds.width
  1174. const nh = initialShapeBounds.height / initialBounds.height
  1175. const minX = bounds.minX + bounds.width * nx
  1176. const minY = bounds.minY + bounds.height * ny
  1177. const width = bounds.width * nw
  1178. const height = bounds.height * nh
  1179. return {
  1180. minX,
  1181. minY,
  1182. maxX: minX + width,
  1183. maxY: minY + height,
  1184. width,
  1185. height,
  1186. }
  1187. }
  1188. export function getShape(
  1189. data: Data,
  1190. shapeId: string,
  1191. pageId = data.currentPageId
  1192. ) {
  1193. return data.document.pages[pageId].shapes[shapeId]
  1194. }
  1195. export function getPage(data: Data, pageId = data.currentPageId) {
  1196. return data.document.pages[pageId]
  1197. }
  1198. export function getCurrentCode(data: Data, fileId = data.currentCodeFileId) {
  1199. return data.document.code[fileId]
  1200. }
  1201. export function getShapes(data: Data, pageId = data.currentPageId) {
  1202. const page = getPage(data, pageId)
  1203. return Object.values(page.shapes)
  1204. }
  1205. export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
  1206. const page = getPage(data, pageId)
  1207. const ids = Array.from(data.selectedIds.values())
  1208. return ids.map((id) => page.shapes[id])
  1209. }
  1210. export function getSelectedBounds(data: Data) {
  1211. return getCommonBounds(
  1212. ...getSelectedShapes(data).map((shape) =>
  1213. getShapeUtils(shape).getBounds(shape)
  1214. )
  1215. )
  1216. }
  1217. export function isMobile() {
  1218. return _isMobile()
  1219. }
  1220. export function getRotatedBounds(shape: Shape) {
  1221. return getShapeUtils(shape).getRotatedBounds(shape)
  1222. }
  1223. export function getShapeBounds(shape: Shape) {
  1224. return getShapeUtils(shape).getBounds(shape)
  1225. }
  1226. export function getBoundsCenter(bounds: Bounds) {
  1227. return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
  1228. }
  1229. export function clampRadians(r: number) {
  1230. return (Math.PI * 2 + r) % (Math.PI * 2)
  1231. }
  1232. export function clampToRotationToSegments(r: number, segments: number) {
  1233. const seg = (Math.PI * 2) / segments
  1234. return Math.floor((clampRadians(r) + seg / 2) / seg) * seg
  1235. }
  1236. export function getParent(data: Data, id: string, pageId = data.currentPageId) {
  1237. const page = getPage(data, pageId)
  1238. const shape = page.shapes[id]
  1239. return page.shapes[shape.parentId] || data.document.pages[shape.parentId]
  1240. }
  1241. export function getChildren(
  1242. data: Data,
  1243. id: string,
  1244. pageId = data.currentPageId
  1245. ) {
  1246. const page = getPage(data, pageId)
  1247. return Object.values(page.shapes)
  1248. .filter(({ parentId }) => parentId === id)
  1249. .sort((a, b) => a.childIndex - b.childIndex)
  1250. }
  1251. export function getSiblings(
  1252. data: Data,
  1253. id: string,
  1254. pageId = data.currentPageId
  1255. ) {
  1256. const page = getPage(data, pageId)
  1257. const shape = page.shapes[id]
  1258. return Object.values(page.shapes)
  1259. .filter(({ parentId }) => parentId === shape.parentId)
  1260. .sort((a, b) => a.childIndex - b.childIndex)
  1261. }
  1262. export function getChildIndexAbove(
  1263. data: Data,
  1264. id: string,
  1265. pageId = data.currentPageId
  1266. ) {
  1267. const page = getPage(data, pageId)
  1268. const shape = page.shapes[id]
  1269. const siblings = Object.values(page.shapes)
  1270. .filter(({ parentId }) => parentId === shape.parentId)
  1271. .sort((a, b) => a.childIndex - b.childIndex)
  1272. const index = siblings.indexOf(shape)
  1273. const nextSibling = siblings[index + 1]
  1274. if (!nextSibling) {
  1275. return shape.childIndex + 1
  1276. }
  1277. let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
  1278. if (nextIndex === nextSibling.childIndex) {
  1279. forceIntegerChildIndices(siblings)
  1280. nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
  1281. }
  1282. return nextIndex
  1283. }
  1284. export function getChildIndexBelow(
  1285. data: Data,
  1286. id: string,
  1287. pageId = data.currentPageId
  1288. ) {
  1289. const page = getPage(data, pageId)
  1290. const shape = page.shapes[id]
  1291. const siblings = Object.values(page.shapes)
  1292. .filter(({ parentId }) => parentId === shape.parentId)
  1293. .sort((a, b) => a.childIndex - b.childIndex)
  1294. const index = siblings.indexOf(shape)
  1295. const prevSibling = siblings[index - 1]
  1296. if (!prevSibling) {
  1297. return shape.childIndex / 2
  1298. }
  1299. let nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
  1300. if (nextIndex === prevSibling.childIndex) {
  1301. forceIntegerChildIndices(siblings)
  1302. nextIndex = (shape.childIndex + prevSibling.childIndex) / 2
  1303. }
  1304. return (shape.childIndex + prevSibling.childIndex) / 2
  1305. }
  1306. export function forceIntegerChildIndices(shapes: Shape[]) {
  1307. for (let i = 0; i < shapes.length; i++) {
  1308. const shape = shapes[i]
  1309. getShapeUtils(shape).setProperty(shape, 'childIndex', i + 1)
  1310. }
  1311. }
  1312. export function setZoomCSS(zoom: number) {
  1313. document.documentElement.style.setProperty('--camera-zoom', zoom.toString())
  1314. }
  1315. export function getCurrent<T extends object>(source: T): T {
  1316. return Object.fromEntries(
  1317. Object.entries(source).map(([key, value]) => [key, value])
  1318. ) as T
  1319. }
  1320. /**
  1321. * Simplify a line (using Ramer-Douglas-Peucker algorithm).
  1322. * @param points An array of points as [x, y, ...][]
  1323. * @param tolerance The minimum line distance (also called epsilon).
  1324. * @returns Simplified array as [x, y, ...][]
  1325. */
  1326. export function simplify(points: number[][], tolerance = 1) {
  1327. const len = points.length,
  1328. a = points[0],
  1329. b = points[len - 1],
  1330. [x1, y1] = a,
  1331. [x2, y2] = b
  1332. if (len > 2) {
  1333. let distance = 0,
  1334. index = 0,
  1335. max = Math.hypot(y2 - y1, x2 - x1)
  1336. for (let i = 1; i < len - 1; i++) {
  1337. const [x0, y0] = points[i],
  1338. d = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
  1339. if (distance > d) continue
  1340. distance = d
  1341. index = i
  1342. }
  1343. if (distance > tolerance) {
  1344. let l0 = simplify(points.slice(0, index + 1), tolerance)
  1345. let l1 = simplify(points.slice(index + 1), tolerance)
  1346. return l0.concat(l1.slice(1))
  1347. }
  1348. }
  1349. return [a, b]
  1350. }
  1351. export function getSvgPathFromStroke(stroke: number[][]) {
  1352. if (!stroke.length) return ''
  1353. const d = stroke.reduce(
  1354. (acc, [x0, y0], i, arr) => {
  1355. const [x1, y1] = arr[(i + 1) % arr.length]
  1356. acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
  1357. return acc
  1358. },
  1359. ['M', ...stroke[0], 'Q']
  1360. )
  1361. d.push('Z')
  1362. return d.join(' ')
  1363. }
  1364. const PI2 = Math.PI * 2
  1365. /**
  1366. * Is angle c between angles a and b?
  1367. * @param a
  1368. * @param b
  1369. * @param c
  1370. */
  1371. export function isAngleBetween(a: number, b: number, c: number) {
  1372. if (c === a || c === b) return true
  1373. const AB = (b - a + PI2) % PI2
  1374. const AC = (c - a + PI2) % PI2
  1375. return AB <= Math.PI !== AC > AB
  1376. }
  1377. export function getCurrentCamera(data: Data) {
  1378. return data.pageStates[data.currentPageId].camera
  1379. }
  1380. // export function updateChildren(data: Data, changedShapes: Shape[]) {
  1381. // if (changedShapes.length === 0) return
  1382. // const { shapes } = getPage(data)
  1383. // changedShapes.forEach((shape) => {
  1384. // if (shape.type === ShapeType.Group) {
  1385. // for (let childId of shape.children) {
  1386. // const childShape = shapes[childId]
  1387. // getShapeUtils(childShape).translateBy(childShape, deltaForShape)
  1388. // }
  1389. // }
  1390. // })
  1391. // }
  1392. /* --------------------- Groups --------------------- */
  1393. export function updateParents(data: Data, changedShapeIds: string[]) {
  1394. if (changedShapeIds.length === 0) return
  1395. const { shapes } = getPage(data)
  1396. const parentToUpdateIds = Array.from(
  1397. new Set(changedShapeIds.map((id) => shapes[id].parentId).values())
  1398. ).filter((id) => id !== data.currentPageId)
  1399. for (const parentId of parentToUpdateIds) {
  1400. const parent = shapes[parentId] as GroupShape
  1401. getShapeUtils(parent).onChildrenChange(
  1402. parent,
  1403. parent.children.map((id) => shapes[id])
  1404. )
  1405. }
  1406. updateParents(data, parentToUpdateIds)
  1407. }
  1408. export function getParentOffset(data: Data, shapeId: string, offset = [0, 0]) {
  1409. const shape = getShape(data, shapeId)
  1410. return shape.parentId === data.currentPageId
  1411. ? offset
  1412. : getParentOffset(data, shape.parentId, vec.add(offset, shape.point))
  1413. }
  1414. export function getParentRotation(
  1415. data: Data,
  1416. shapeId: string,
  1417. rotation = 0
  1418. ): number {
  1419. const shape = getShape(data, shapeId)
  1420. return shape.parentId === data.currentPageId
  1421. ? rotation + shape.rotation
  1422. : getParentRotation(data, shape.parentId, rotation + shape.rotation)
  1423. }
  1424. export function getDocumentBranch(data: Data, id: string): string[] {
  1425. const shape = getPage(data).shapes[id]
  1426. if (shape.type !== ShapeType.Group) return [id]
  1427. return [
  1428. id,
  1429. ...shape.children.flatMap((childId) => getDocumentBranch(data, childId)),
  1430. ]
  1431. }