Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

utils.ts 37KB

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