Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

utils.ts 36KB

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