Browse Source

Updates to code editor, utils

main
Steve Ruiz 4 years ago
parent
commit
69bdab520a

+ 23
- 14
components/code-panel/code-editor.tsx View File

@@ -81,20 +81,29 @@ export default function CodeEditor({
81 81
 
82 82
     monaco.languages.registerDocumentFormattingEditProvider('typescript', {
83 83
       async provideDocumentFormattingEdits(model) {
84
-        const text = prettier.format(model.getValue(), {
85
-          parser: 'typescript',
86
-          plugins: [parserTypeScript],
87
-          singleQuote: true,
88
-          trailingComma: 'es5',
89
-          semi: false,
90
-        })
91
-
92
-        return [
93
-          {
94
-            range: model.getFullModelRange(),
95
-            text,
96
-          },
97
-        ]
84
+        try {
85
+          const text = prettier.format(model.getValue(), {
86
+            parser: 'typescript',
87
+            plugins: [parserTypeScript],
88
+            singleQuote: true,
89
+            trailingComma: 'es5',
90
+            semi: false,
91
+          })
92
+
93
+          return [
94
+            {
95
+              range: model.getFullModelRange(),
96
+              text,
97
+            },
98
+          ]
99
+        } catch (e) {
100
+          return [
101
+            {
102
+              range: model.getFullModelRange(),
103
+              text: model.getValue(),
104
+            },
105
+          ]
106
+        }
98 107
       },
99 108
     })
100 109
   }, [])

+ 3
- 2
components/code-panel/code-panel.tsx View File

@@ -5,7 +5,6 @@ import React, { useEffect, useRef } from 'react'
5 5
 import state, { useSelector } from 'state'
6 6
 import { CodeFile } from 'types'
7 7
 import CodeDocs from './code-docs'
8
-import CodeEditor from './code-editor'
9 8
 import { generateFromCode } from 'state/code/generate'
10 9
 import * as Panel from '../panel'
11 10
 import { IconButton } from '../shared'
@@ -17,6 +16,8 @@ import {
17 16
   ChevronUp,
18 17
   ChevronDown,
19 18
 } from 'react-feather'
19
+import dynamic from 'next/dynamic'
20
+const CodeEditor = dynamic(() => import('./code-editor'))
20 21
 
21 22
 const getErrorLineAndColumn = (e: any) => {
22 23
   if ('line' in e) {
@@ -84,7 +85,7 @@ export default function CodePanel(): JSX.Element {
84 85
           const { shapes, controls } = generateFromCode(state.data, data.code)
85 86
           state.send('GENERATED_FROM_CODE', { shapes, controls })
86 87
         } catch (e) {
87
-          console.error(e)
88
+          console.error('Got an error!', e)
88 89
           error = { message: e.message, ...getErrorLineAndColumn(e) }
89 90
         }
90 91
 

+ 605
- 38
components/code-panel/types-import.ts View File

@@ -199,12 +199,10 @@ interface GroupShape extends BaseShape {
199 199
   size: number[]
200 200
 }
201 201
 
202
-type DeepPartial<T> = {
203
-  [P in keyof T]?: DeepPartial<T[P]>
202
+type ShapeProps<T extends Shape> = Partial<T> & {
203
+  style?: Partial<ShapeStyles>
204 204
 }
205 205
 
206
-type ShapeProps<T extends Shape> = DeepPartial<T>
207
-
208 206
 type MutableShape =
209 207
   | DotShape
210 208
   | EllipseShape
@@ -817,7 +815,7 @@ interface ShapeUtility<K extends Shape> {
817 815
  * ## Rectangle
818 816
  */
819 817
  class Rectangle extends CodeShape<RectangleShape> {
820
-  constructor(props = {} as Partial<RectangleShape> & Partial<ShapeStyles>) {
818
+  constructor(props = {} as ShapeProps<RectangleShape>) {
821 819
     super({
822 820
       id: uniqueId(),
823 821
       seed: Math.random(),
@@ -970,69 +968,522 @@ interface ShapeUtility<K extends Shape> {
970 968
  * ## Utils
971 969
  */
972 970
  class Utils {
973
-  static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
974
-    return Array.from(Array(steps))
975
-      .map((_, i) => {
976
-        const t = i / steps
977
-        return t * t * t
978
-      })
979
-      .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
971
+  /**
972
+   * Linear interpolation betwen two numbers.
973
+   * @param y1
974
+   * @param y2
975
+   * @param mu
976
+   */
977
+  static lerp(y1: number, y2: number, mu: number): number {
978
+    mu = Utils.clamp(mu, 0, 1)
979
+    return y1 * (1 - mu) + y2 * mu
980 980
   }
981 981
 
982
-  static getRayRayIntersection(
983
-    p0: number[],
984
-    n0: number[],
985
-    p1: number[],
986
-    n1: number[]
987
-  ): number[] {
988
-    const p0e = vec.add(p0, n0),
989
-      p1e = vec.add(p1, n1),
990
-      m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
991
-      m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
992
-      b0 = p0[1] - m0 * p0[0],
993
-      b1 = p1[1] - m1 * p1[0],
994
-      x = (b1 - b0) / (m0 - m1),
995
-      y = m0 * x + b0
982
+  /**
983
+   * Modulate a value between two ranges.
984
+   * @param value
985
+   * @param rangeA from [low, high]
986
+   * @param rangeB to [low, high]
987
+   * @param clamp
988
+   */
989
+  static modulate(
990
+    value: number,
991
+    rangeA: number[],
992
+    rangeB: number[],
993
+    clamp = false
994
+  ): number {
995
+    const [fromLow, fromHigh] = rangeA
996
+    const [v0, v1] = rangeB
997
+    const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
998
+
999
+    return clamp
1000
+      ? v0 < v1
1001
+        ? Math.max(Math.min(result, v1), v0)
1002
+        : Math.max(Math.min(result, v0), v1)
1003
+      : result
1004
+  }
996 1005
 
997
-    return [x, y]
1006
+  /**
1007
+   * Clamp a value into a range.
1008
+   * @param n
1009
+   * @param min
1010
+   */
1011
+  static clamp(n: number, min: number): number
1012
+  static clamp(n: number, min: number, max: number): number
1013
+  static clamp(n: number, min: number, max?: number): number {
1014
+    return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
1015
+  }
1016
+
1017
+  // TODO: replace with a string compression algorithm
1018
+  static compress(s: string): string {
1019
+    return s
1020
+  }
1021
+
1022
+  // TODO: replace with a string decompression algorithm
1023
+  static decompress(s: string): string {
1024
+    return s
1025
+  }
1026
+
1027
+  /**
1028
+   * Recursively clone an object or array.
1029
+   * @param obj
1030
+   */
1031
+  static deepClone<T>(obj: T): T {
1032
+    if (obj === null) return null
1033
+
1034
+    const clone: any = { ...obj }
1035
+
1036
+    Object.keys(obj).forEach(
1037
+      (key) =>
1038
+        (clone[key] =
1039
+          typeof obj[key] === 'object' ? Utils.deepClone(obj[key]) : obj[key])
1040
+    )
1041
+
1042
+    if (Array.isArray(obj)) {
1043
+      clone.length = obj.length
1044
+      return Array.from(clone) as any as T
1045
+    }
1046
+
1047
+    return clone as T
1048
+  }
1049
+
1050
+  /**
1051
+   * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift).
1052
+   * The result will always be betweeen -1 and 1.
1053
+   *
1054
+   * Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
1055
+   */
1056
+  static rng(seed = ''): () => number {
1057
+    let x = 0
1058
+    let y = 0
1059
+    let z = 0
1060
+    let w = 0
1061
+
1062
+    function next() {
1063
+      const t = x ^ (x << 11)
1064
+      ;(x = y), (y = z), (z = w)
1065
+      w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0
1066
+      return w / 0x100000000
1067
+    }
1068
+
1069
+    for (let k = 0; k < seed.length + 64; k++) {
1070
+      ;(x ^= seed.charCodeAt(k) | 0), next()
1071
+    }
1072
+
1073
+    return next
1074
+  }
1075
+
1076
+  /**
1077
+   * Shuffle the contents of an array.
1078
+   * @param arr
1079
+   * @param offset
1080
+   */
1081
+  static shuffleArr<T>(arr: T[], offset: number): T[] {
1082
+    return arr.map((_, i) => arr[(i + offset) % arr.length])
1083
+  }
1084
+
1085
+  /**
1086
+   * Deep compare two arrays.
1087
+   * @param a
1088
+   * @param b
1089
+   */
1090
+  static deepCompareArrays<T>(a: T[], b: T[]): boolean {
1091
+    if (a?.length !== b?.length) return false
1092
+    return Utils.deepCompare(a, b)
998 1093
   }
999 1094
 
1095
+  /**
1096
+   * Deep compare any values.
1097
+   * @param a
1098
+   * @param b
1099
+   */
1100
+  static deepCompare<T>(a: T, b: T): boolean {
1101
+    return a === b || JSON.stringify(a) === JSON.stringify(b)
1102
+  }
1103
+
1104
+  /**
1105
+   * Find whether two arrays intersect.
1106
+   * @param a
1107
+   * @param b
1108
+   * @param fn An optional function to apply to the items of a; will check if b includes the result.
1109
+   */
1110
+  static arrsIntersect<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
1111
+  static arrsIntersect<T>(a: T[], b: T[]): boolean
1112
+  static arrsIntersect<T>(
1113
+    a: T[],
1114
+    b: unknown[],
1115
+    fn?: (item: unknown) => T
1116
+  ): boolean {
1117
+    return a.some((item) => b.includes(fn ? fn(item) : item))
1118
+  }
1119
+
1120
+  /**
1121
+   * Get the unique values from an array of strings or numbers.
1122
+   * @param items
1123
+   */
1124
+  static uniqueArray<T extends string | number>(...items: T[]): T[] {
1125
+    return Array.from(new Set(items).values())
1126
+  }
1127
+
1128
+  /**
1129
+   * Convert a set to an array.
1130
+   * @param set
1131
+   */
1132
+  static setToArray<T>(set: Set<T>): T[] {
1133
+    return Array.from(set.values())
1134
+  }
1135
+
1136
+  /**
1137
+   * Get the outer of between a circle and a point.
1138
+   * @param C The circle's center.
1139
+   * @param r The circle's radius.
1140
+   * @param P The point.
1141
+   * @param side
1142
+   */
1000 1143
   static getCircleTangentToPoint(
1001
-    A: number[],
1002
-    r0: number,
1144
+    C: number[],
1145
+    r: number,
1003 1146
     P: number[],
1004 1147
     side: number
1005 1148
   ): number[] {
1006
-    const B = vec.lrp(A, P, 0.5),
1007
-      r1 = vec.dist(A, B),
1008
-      delta = vec.sub(B, A),
1149
+    const B = vec.lrp(C, P, 0.5),
1150
+      r1 = vec.dist(C, B),
1151
+      delta = vec.sub(B, C),
1009 1152
       d = vec.len(delta)
1010 1153
 
1011
-    if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
1154
+    if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
1012 1155
       return
1013 1156
     }
1014 1157
 
1015
-    const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
1158
+    const a = (r * r - r1 * r1 + d * d) / (2.0 * d),
1016 1159
       n = 1 / d,
1017
-      p = vec.add(A, vec.mul(delta, a * n)),
1018
-      h = Math.sqrt(r0 * r0 - a * a),
1160
+      p = vec.add(C, vec.mul(delta, a * n)),
1161
+      h = Math.sqrt(r * r - a * a),
1019 1162
       k = vec.mul(vec.per(delta), h * n)
1020 1163
 
1021 1164
     return side === 0 ? vec.add(p, k) : vec.sub(p, k)
1022 1165
   }
1023 1166
 
1167
+  /**
1168
+   * Get outer tangents of two circles.
1169
+   * @param x0
1170
+   * @param y0
1171
+   * @param r0
1172
+   * @param x1
1173
+   * @param y1
1174
+   * @param r1
1175
+   * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
1176
+   */
1177
+  static getOuterTangentsOfCircles(
1178
+    C0: number[],
1179
+    r0: number,
1180
+    C1: number[],
1181
+    r1: number
1182
+  ): number[][] {
1183
+    const a0 = vec.angle(C0, C1)
1184
+    const d = vec.dist(C0, C1)
1185
+
1186
+    // Circles are overlapping, no tangents
1187
+    if (d < Math.abs(r1 - r0)) return
1188
+
1189
+    const a1 = Math.acos((r0 - r1) / d),
1190
+      t0 = a0 + a1,
1191
+      t1 = a0 - a1
1192
+
1193
+    return [
1194
+      [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
1195
+      [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
1196
+      [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
1197
+      [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
1198
+    ]
1199
+  }
1200
+
1201
+  /**
1202
+   * Get the closest point on the perimeter of a circle to a given point.
1203
+   * @param C The circle's center.
1204
+   * @param r The circle's radius.
1205
+   * @param P The point.
1206
+   */
1207
+  static getClosestPointOnCircle(
1208
+    C: number[],
1209
+    r: number,
1210
+    P: number[]
1211
+  ): number[] {
1212
+    const v = vec.sub(C, P)
1213
+    return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
1214
+  }
1215
+
1216
+  static det(
1217
+    a: number,
1218
+    b: number,
1219
+    c: number,
1220
+    d: number,
1221
+    e: number,
1222
+    f: number,
1223
+    g: number,
1224
+    h: number,
1225
+    i: number
1226
+  ): number {
1227
+    return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
1228
+  }
1229
+
1230
+  /**
1231
+   * Get a circle from three points.
1232
+   * @param A
1233
+   * @param B
1234
+   * @param C
1235
+   * @returns [x, y, r]
1236
+   */
1237
+  static circleFromThreePoints(
1238
+    A: number[],
1239
+    B: number[],
1240
+    C: number[]
1241
+  ): number[] {
1242
+    const a = Utils.det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
1243
+
1244
+    const bx = -Utils.det(
1245
+      A[0] * A[0] + A[1] * A[1],
1246
+      A[1],
1247
+      1,
1248
+      B[0] * B[0] + B[1] * B[1],
1249
+      B[1],
1250
+      1,
1251
+      C[0] * C[0] + C[1] * C[1],
1252
+      C[1],
1253
+      1
1254
+    )
1255
+    const by = Utils.det(
1256
+      A[0] * A[0] + A[1] * A[1],
1257
+      A[0],
1258
+      1,
1259
+      B[0] * B[0] + B[1] * B[1],
1260
+      B[0],
1261
+      1,
1262
+      C[0] * C[0] + C[1] * C[1],
1263
+      C[0],
1264
+      1
1265
+    )
1266
+    const c = -Utils.det(
1267
+      A[0] * A[0] + A[1] * A[1],
1268
+      A[0],
1269
+      A[1],
1270
+      B[0] * B[0] + B[1] * B[1],
1271
+      B[0],
1272
+      B[1],
1273
+      C[0] * C[0] + C[1] * C[1],
1274
+      C[0],
1275
+      C[1]
1276
+    )
1277
+
1278
+    const x = -bx / (2 * a)
1279
+    const y = -by / (2 * a)
1280
+    const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
1281
+
1282
+    return [x, y, r]
1283
+  }
1284
+
1285
+  /**
1286
+   * Find the approximate perimeter of an ellipse.
1287
+   * @param rx
1288
+   * @param ry
1289
+   */
1290
+  static perimeterOfEllipse(rx: number, ry: number): number {
1291
+    const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
1292
+    const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
1293
+    return p
1294
+  }
1295
+
1296
+  /**
1297
+   * Get the short angle distance between two angles.
1298
+   * @param a0
1299
+   * @param a1
1300
+   */
1024 1301
   static shortAngleDist(a0: number, a1: number): number {
1025 1302
     const max = Math.PI * 2
1026 1303
     const da = (a1 - a0) % max
1027 1304
     return ((2 * da) % max) - da
1028 1305
   }
1029 1306
 
1307
+  /**
1308
+   * Get the long angle distance between two angles.
1309
+   * @param a0
1310
+   * @param a1
1311
+   */
1312
+  static longAngleDist(a0: number, a1: number): number {
1313
+    return Math.PI * 2 - Utils.shortAngleDist(a0, a1)
1314
+  }
1315
+
1316
+  /**
1317
+   * Interpolate an angle between two angles.
1318
+   * @param a0
1319
+   * @param a1
1320
+   * @param t
1321
+   */
1322
+  static lerpAngles(a0: number, a1: number, t: number): number {
1323
+    return a0 + Utils.shortAngleDist(a0, a1) * t
1324
+  }
1325
+
1326
+  /**
1327
+   * Get the short distance between two angles.
1328
+   * @param a0
1329
+   * @param a1
1330
+   */
1030 1331
   static angleDelta(a0: number, a1: number): number {
1031
-    return this.shortAngleDist(a0, a1)
1332
+    return Utils.shortAngleDist(a0, a1)
1032 1333
   }
1033 1334
 
1335
+  /**
1336
+   * Get the "sweep" or short distance between two points on a circle's perimeter.
1337
+   * @param C
1338
+   * @param A
1339
+   * @param B
1340
+   */
1034 1341
   static getSweep(C: number[], A: number[], B: number[]): number {
1035
-    return this.angleDelta(vec.angle(C, A), vec.angle(C, B))
1342
+    return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B))
1343
+  }
1344
+
1345
+  /**
1346
+   * Rotate a point around a center.
1347
+   * @param x The x-axis coordinate of the point.
1348
+   * @param y The y-axis coordinate of the point.
1349
+   * @param cx The x-axis coordinate of the point to rotate round.
1350
+   * @param cy The y-axis coordinate of the point to rotate round.
1351
+   * @param angle The distance (in radians) to rotate.
1352
+   */
1353
+  static rotatePoint(A: number[], B: number[], angle: number): number[] {
1354
+    const s = Math.sin(angle)
1355
+    const c = Math.cos(angle)
1356
+
1357
+    const px = A[0] - B[0]
1358
+    const py = A[1] - B[1]
1359
+
1360
+    const nx = px * c - py * s
1361
+    const ny = px * s + py * c
1362
+
1363
+    return [nx + B[0], ny + B[1]]
1364
+  }
1365
+
1366
+  /**
1367
+   * Clamp radians within 0 and 2PI
1368
+   * @param r
1369
+   */
1370
+  static clampRadians(r: number): number {
1371
+    return (Math.PI * 2 + r) % (Math.PI * 2)
1372
+  }
1373
+
1374
+  /**
1375
+   * Clamp rotation to even segments.
1376
+   * @param r
1377
+   * @param segments
1378
+   */
1379
+  static clampToRotationToSegments(r: number, segments: number): number {
1380
+    const seg = (Math.PI * 2) / segments
1381
+    return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg
1382
+  }
1383
+
1384
+  /**
1385
+   * Is angle c between angles a and b?
1386
+   * @param a
1387
+   * @param b
1388
+   * @param c
1389
+   */
1390
+  static isAngleBetween(a: number, b: number, c: number): boolean {
1391
+    if (c === a || c === b) return true
1392
+    const PI2 = Math.PI * 2
1393
+    const AB = (b - a + PI2) % PI2
1394
+    const AC = (c - a + PI2) % PI2
1395
+    return AB <= Math.PI !== AC > AB
1396
+  }
1397
+
1398
+  /**
1399
+   * Convert degrees to radians.
1400
+   * @param d
1401
+   */
1402
+  static degreesToRadians(d: number): number {
1403
+    return (d * Math.PI) / 180
1404
+  }
1405
+
1406
+  /**
1407
+   * Convert radians to degrees.
1408
+   * @param r
1409
+   */
1410
+  static radiansToDegrees(r: number): number {
1411
+    return (r * 180) / Math.PI
1412
+  }
1413
+
1414
+  /**
1415
+   * Get the length of an arc between two points on a circle's perimeter.
1416
+   * @param C
1417
+   * @param r
1418
+   * @param A
1419
+   * @param B
1420
+   */
1421
+  static getArcLength(
1422
+    C: number[],
1423
+    r: number,
1424
+    A: number[],
1425
+    B: number[]
1426
+  ): number {
1427
+    const sweep = Utils.getSweep(C, A, B)
1428
+    return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
1429
+  }
1430
+
1431
+  /**
1432
+   * Get a dash offset for an arc, based on its length.
1433
+   * @param C
1434
+   * @param r
1435
+   * @param A
1436
+   * @param B
1437
+   * @param step
1438
+   */
1439
+  static getArcDashOffset(
1440
+    C: number[],
1441
+    r: number,
1442
+    A: number[],
1443
+    B: number[],
1444
+    step: number
1445
+  ): number {
1446
+    const del0 = Utils.getSweep(C, A, B)
1447
+    const len0 = Utils.getArcLength(C, r, A, B)
1448
+    const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
1449
+    return -off0 / 2 + step
1450
+  }
1451
+
1452
+  /**
1453
+   * Get a dash offset for an ellipse, based on its length.
1454
+   * @param A
1455
+   * @param step
1456
+   */
1457
+  static getEllipseDashOffset(A: number[], step: number): number {
1458
+    const c = 2 * Math.PI * A[2]
1459
+    return -c / 2 + -step
1460
+  }
1461
+
1462
+  static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
1463
+    return Array.from(Array(steps))
1464
+      .map((_, i) => {
1465
+        const t = i / steps
1466
+        return t * t * t
1467
+      })
1468
+      .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
1469
+  }
1470
+
1471
+  static getRayRayIntersection(
1472
+    p0: number[],
1473
+    n0: number[],
1474
+    p1: number[],
1475
+    n1: number[]
1476
+  ): number[] {
1477
+    const p0e = vec.add(p0, n0),
1478
+      p1e = vec.add(p1, n1),
1479
+      m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
1480
+      m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
1481
+      b0 = p0[1] - m0 * p0[0],
1482
+      b1 = p1[1] - m1 * p1[0],
1483
+      x = (b1 - b0) / (m0 - m1),
1484
+      y = m0 * x + b0
1485
+
1486
+    return [x, y]
1036 1487
   }
1037 1488
 
1038 1489
   static bez1d(a: number, b: number, c: number, d: number, t: number): number {
@@ -1132,6 +1583,122 @@ interface ShapeUtility<K extends Shape> {
1132 1583
 
1133 1584
     return bounds
1134 1585
   }
1586
+
1587
+  /**
1588
+   * Get a bezier curve data for a spline that fits an array of points.
1589
+   * @param pts
1590
+   * @param tension
1591
+   * @param isClosed
1592
+   * @param numOfSegments
1593
+   */
1594
+  static getCurvePoints(
1595
+    pts: number[][],
1596
+    tension = 0.5,
1597
+    isClosed = false,
1598
+    numOfSegments = 3
1599
+  ): number[][] {
1600
+    const _pts = [...pts],
1601
+      len = pts.length,
1602
+      res: number[][] = [] // results
1603
+
1604
+    let t1x: number, // tension vectors
1605
+      t2x: number,
1606
+      t1y: number,
1607
+      t2y: number,
1608
+      c1: number, // cardinal points
1609
+      c2: number,
1610
+      c3: number,
1611
+      c4: number,
1612
+      st: number,
1613
+      st2: number,
1614
+      st3: number
1615
+
1616
+    // The algorithm require a previous and next point to the actual point array.
1617
+    // Check if we will draw closed or open curve.
1618
+    // If closed, copy end points to beginning and first points to end
1619
+    // If open, duplicate first points to befinning, end points to end
1620
+    if (isClosed) {
1621
+      _pts.unshift(_pts[len - 1])
1622
+      _pts.push(_pts[0])
1623
+    } else {
1624
+      //copy 1. point and insert at beginning
1625
+      _pts.unshift(_pts[0])
1626
+      _pts.push(_pts[len - 1])
1627
+      // _pts.push(_pts[len - 1])
1628
+    }
1629
+
1630
+    // For each point, calculate a segment
1631
+    for (let i = 1; i < _pts.length - 2; i++) {
1632
+      // Calculate points along segment and add to results
1633
+      for (let t = 0; t <= numOfSegments; t++) {
1634
+        // Step
1635
+        st = t / numOfSegments
1636
+        st2 = Math.pow(st, 2)
1637
+        st3 = Math.pow(st, 3)
1638
+
1639
+        // Cardinals
1640
+        c1 = 2 * st3 - 3 * st2 + 1
1641
+        c2 = -(2 * st3) + 3 * st2
1642
+        c3 = st3 - 2 * st2 + st
1643
+        c4 = st3 - st2
1644
+
1645
+        // Tension
1646
+        t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
1647
+        t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
1648
+        t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
1649
+        t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
1650
+
1651
+        // Control points
1652
+        res.push([
1653
+          c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
1654
+          c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
1655
+        ])
1656
+      }
1657
+    }
1658
+
1659
+    res.push(pts[pts.length - 1])
1660
+
1661
+    return res
1662
+  }
1663
+
1664
+  /**
1665
+   * Simplify a line (using Ramer-Douglas-Peucker algorithm).
1666
+   * @param points An array of points as [x, y, ...][]
1667
+   * @param tolerance The minimum line distance (also called epsilon).
1668
+   * @returns Simplified array as [x, y, ...][]
1669
+   */
1670
+  static simplify(points: number[][], tolerance = 1): number[][] {
1671
+    const len = points.length,
1672
+      a = points[0],
1673
+      b = points[len - 1],
1674
+      [x1, y1] = a,
1675
+      [x2, y2] = b
1676
+
1677
+    if (len > 2) {
1678
+      let distance = 0
1679
+      let index = 0
1680
+      const max = Math.hypot(y2 - y1, x2 - x1)
1681
+
1682
+      for (let i = 1; i < len - 1; i++) {
1683
+        const [x0, y0] = points[i],
1684
+          d =
1685
+            Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
1686
+
1687
+        if (distance > d) continue
1688
+
1689
+        distance = d
1690
+        index = i
1691
+      }
1692
+
1693
+      if (distance > tolerance) {
1694
+        const l0 = Utils.simplify(points.slice(0, index + 1), tolerance)
1695
+        const l1 = Utils.simplify(points.slice(index + 1), tolerance)
1696
+        return l0.concat(l1.slice(1))
1697
+      }
1698
+    }
1699
+
1700
+    return [a, b]
1701
+  }
1135 1702
 }
1136 1703
 
1137 1704
 // A big collection of vector utilities. Collected into a class to improve logging / packaging.

+ 1
- 0
package.json View File

@@ -61,6 +61,7 @@
61 61
     "react-dom": "^17.0.2",
62 62
     "react-feather": "^2.0.9",
63 63
     "react-use-gesture": "^9.1.3",
64
+    "sucrase": "^3.19.0",
64 65
     "uuid": "^8.3.2"
65 66
   },
66 67
   "devDependencies": {

+ 4
- 1
state/code/generate.ts View File

@@ -12,6 +12,7 @@ import { NumberControl, VectorControl, codeControls, controls } from './control'
12 12
 import { codeShapes } from './index'
13 13
 import { CodeControl, Data, Shape } from 'types'
14 14
 import { getPage } from 'utils'
15
+import { transform } from 'sucrase'
15 16
 
16 17
 const baseScope = {
17 18
   Dot,
@@ -48,7 +49,9 @@ export function generateFromCode(
48 49
   const { currentPageId } = data
49 50
   const scope = { ...baseScope, controls, currentPageId }
50 51
 
51
-  new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
52
+  const transformed = transform(code, { transforms: ['typescript'] }).code
53
+
54
+  new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope))
52 55
 
53 56
   const generatedShapes = Array.from(codeShapes.values()).map((instance) => ({
54 57
     ...instance.shape,

+ 2
- 2
state/code/rectangle.ts View File

@@ -1,13 +1,13 @@
1 1
 import CodeShape from './index'
2 2
 import { uniqueId } from 'utils'
3
-import { RectangleShape, ShapeStyles, ShapeType } from 'types'
3
+import { RectangleShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 
6 6
 /**
7 7
  * ## Rectangle
8 8
  */
9 9
 export default class Rectangle extends CodeShape<RectangleShape> {
10
-  constructor(props = {} as Partial<RectangleShape> & Partial<ShapeStyles>) {
10
+  constructor(props = {} as ShapeProps<RectangleShape>) {
11 11
     super({
12 12
       id: uniqueId(),
13 13
       seed: Math.random(),

+ 602
- 33
state/code/utils.ts View File

@@ -5,69 +5,522 @@ import vec from 'utils/vec'
5 5
  * ## Utils
6 6
  */
7 7
 export default class Utils {
8
-  static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
9
-    return Array.from(Array(steps))
10
-      .map((_, i) => {
11
-        const t = i / steps
12
-        return t * t * t
13
-      })
14
-      .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
8
+  /**
9
+   * Linear interpolation betwen two numbers.
10
+   * @param y1
11
+   * @param y2
12
+   * @param mu
13
+   */
14
+  static lerp(y1: number, y2: number, mu: number): number {
15
+    mu = Utils.clamp(mu, 0, 1)
16
+    return y1 * (1 - mu) + y2 * mu
15 17
   }
16 18
 
17
-  static getRayRayIntersection(
18
-    p0: number[],
19
-    n0: number[],
20
-    p1: number[],
21
-    n1: number[]
22
-  ): number[] {
23
-    const p0e = vec.add(p0, n0),
24
-      p1e = vec.add(p1, n1),
25
-      m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
26
-      m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
27
-      b0 = p0[1] - m0 * p0[0],
28
-      b1 = p1[1] - m1 * p1[0],
29
-      x = (b1 - b0) / (m0 - m1),
30
-      y = m0 * x + b0
19
+  /**
20
+   * Modulate a value between two ranges.
21
+   * @param value
22
+   * @param rangeA from [low, high]
23
+   * @param rangeB to [low, high]
24
+   * @param clamp
25
+   */
26
+  static modulate(
27
+    value: number,
28
+    rangeA: number[],
29
+    rangeB: number[],
30
+    clamp = false
31
+  ): number {
32
+    const [fromLow, fromHigh] = rangeA
33
+    const [v0, v1] = rangeB
34
+    const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
31 35
 
32
-    return [x, y]
36
+    return clamp
37
+      ? v0 < v1
38
+        ? Math.max(Math.min(result, v1), v0)
39
+        : Math.max(Math.min(result, v0), v1)
40
+      : result
41
+  }
42
+
43
+  /**
44
+   * Clamp a value into a range.
45
+   * @param n
46
+   * @param min
47
+   */
48
+  static clamp(n: number, min: number): number
49
+  static clamp(n: number, min: number, max: number): number
50
+  static clamp(n: number, min: number, max?: number): number {
51
+    return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
52
+  }
53
+
54
+  // TODO: replace with a string compression algorithm
55
+  static compress(s: string): string {
56
+    return s
57
+  }
58
+
59
+  // TODO: replace with a string decompression algorithm
60
+  static decompress(s: string): string {
61
+    return s
62
+  }
63
+
64
+  /**
65
+   * Recursively clone an object or array.
66
+   * @param obj
67
+   */
68
+  static deepClone<T>(obj: T): T {
69
+    if (obj === null) return null
70
+
71
+    const clone: any = { ...obj }
72
+
73
+    Object.keys(obj).forEach(
74
+      (key) =>
75
+        (clone[key] =
76
+          typeof obj[key] === 'object' ? Utils.deepClone(obj[key]) : obj[key])
77
+    )
78
+
79
+    if (Array.isArray(obj)) {
80
+      clone.length = obj.length
81
+      return Array.from(clone) as any as T
82
+    }
83
+
84
+    return clone as T
33 85
   }
34 86
 
87
+  /**
88
+   * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift).
89
+   * The result will always be betweeen -1 and 1.
90
+   *
91
+   * Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
92
+   */
93
+  static rng(seed = ''): () => number {
94
+    let x = 0
95
+    let y = 0
96
+    let z = 0
97
+    let w = 0
98
+
99
+    function next() {
100
+      const t = x ^ (x << 11)
101
+      ;(x = y), (y = z), (z = w)
102
+      w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0
103
+      return w / 0x100000000
104
+    }
105
+
106
+    for (let k = 0; k < seed.length + 64; k++) {
107
+      ;(x ^= seed.charCodeAt(k) | 0), next()
108
+    }
109
+
110
+    return next
111
+  }
112
+
113
+  /**
114
+   * Shuffle the contents of an array.
115
+   * @param arr
116
+   * @param offset
117
+   */
118
+  static shuffleArr<T>(arr: T[], offset: number): T[] {
119
+    return arr.map((_, i) => arr[(i + offset) % arr.length])
120
+  }
121
+
122
+  /**
123
+   * Deep compare two arrays.
124
+   * @param a
125
+   * @param b
126
+   */
127
+  static deepCompareArrays<T>(a: T[], b: T[]): boolean {
128
+    if (a?.length !== b?.length) return false
129
+    return Utils.deepCompare(a, b)
130
+  }
131
+
132
+  /**
133
+   * Deep compare any values.
134
+   * @param a
135
+   * @param b
136
+   */
137
+  static deepCompare<T>(a: T, b: T): boolean {
138
+    return a === b || JSON.stringify(a) === JSON.stringify(b)
139
+  }
140
+
141
+  /**
142
+   * Find whether two arrays intersect.
143
+   * @param a
144
+   * @param b
145
+   * @param fn An optional function to apply to the items of a; will check if b includes the result.
146
+   */
147
+  static arrsIntersect<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
148
+  static arrsIntersect<T>(a: T[], b: T[]): boolean
149
+  static arrsIntersect<T>(
150
+    a: T[],
151
+    b: unknown[],
152
+    fn?: (item: unknown) => T
153
+  ): boolean {
154
+    return a.some((item) => b.includes(fn ? fn(item) : item))
155
+  }
156
+
157
+  /**
158
+   * Get the unique values from an array of strings or numbers.
159
+   * @param items
160
+   */
161
+  static uniqueArray<T extends string | number>(...items: T[]): T[] {
162
+    return Array.from(new Set(items).values())
163
+  }
164
+
165
+  /**
166
+   * Convert a set to an array.
167
+   * @param set
168
+   */
169
+  static setToArray<T>(set: Set<T>): T[] {
170
+    return Array.from(set.values())
171
+  }
172
+
173
+  /**
174
+   * Get the outer of between a circle and a point.
175
+   * @param C The circle's center.
176
+   * @param r The circle's radius.
177
+   * @param P The point.
178
+   * @param side
179
+   */
35 180
   static getCircleTangentToPoint(
36
-    A: number[],
37
-    r0: number,
181
+    C: number[],
182
+    r: number,
38 183
     P: number[],
39 184
     side: number
40 185
   ): number[] {
41
-    const B = vec.lrp(A, P, 0.5),
42
-      r1 = vec.dist(A, B),
43
-      delta = vec.sub(B, A),
186
+    const B = vec.lrp(C, P, 0.5),
187
+      r1 = vec.dist(C, B),
188
+      delta = vec.sub(B, C),
44 189
       d = vec.len(delta)
45 190
 
46
-    if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
191
+    if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
47 192
       return
48 193
     }
49 194
 
50
-    const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
195
+    const a = (r * r - r1 * r1 + d * d) / (2.0 * d),
51 196
       n = 1 / d,
52
-      p = vec.add(A, vec.mul(delta, a * n)),
53
-      h = Math.sqrt(r0 * r0 - a * a),
197
+      p = vec.add(C, vec.mul(delta, a * n)),
198
+      h = Math.sqrt(r * r - a * a),
54 199
       k = vec.mul(vec.per(delta), h * n)
55 200
 
56 201
     return side === 0 ? vec.add(p, k) : vec.sub(p, k)
57 202
   }
58 203
 
204
+  /**
205
+   * Get outer tangents of two circles.
206
+   * @param x0
207
+   * @param y0
208
+   * @param r0
209
+   * @param x1
210
+   * @param y1
211
+   * @param r1
212
+   * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
213
+   */
214
+  static getOuterTangentsOfCircles(
215
+    C0: number[],
216
+    r0: number,
217
+    C1: number[],
218
+    r1: number
219
+  ): number[][] {
220
+    const a0 = vec.angle(C0, C1)
221
+    const d = vec.dist(C0, C1)
222
+
223
+    // Circles are overlapping, no tangents
224
+    if (d < Math.abs(r1 - r0)) return
225
+
226
+    const a1 = Math.acos((r0 - r1) / d),
227
+      t0 = a0 + a1,
228
+      t1 = a0 - a1
229
+
230
+    return [
231
+      [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
232
+      [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
233
+      [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
234
+      [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
235
+    ]
236
+  }
237
+
238
+  /**
239
+   * Get the closest point on the perimeter of a circle to a given point.
240
+   * @param C The circle's center.
241
+   * @param r The circle's radius.
242
+   * @param P The point.
243
+   */
244
+  static getClosestPointOnCircle(
245
+    C: number[],
246
+    r: number,
247
+    P: number[]
248
+  ): number[] {
249
+    const v = vec.sub(C, P)
250
+    return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
251
+  }
252
+
253
+  static det(
254
+    a: number,
255
+    b: number,
256
+    c: number,
257
+    d: number,
258
+    e: number,
259
+    f: number,
260
+    g: number,
261
+    h: number,
262
+    i: number
263
+  ): number {
264
+    return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
265
+  }
266
+
267
+  /**
268
+   * Get a circle from three points.
269
+   * @param A
270
+   * @param B
271
+   * @param C
272
+   * @returns [x, y, r]
273
+   */
274
+  static circleFromThreePoints(
275
+    A: number[],
276
+    B: number[],
277
+    C: number[]
278
+  ): number[] {
279
+    const a = Utils.det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
280
+
281
+    const bx = -Utils.det(
282
+      A[0] * A[0] + A[1] * A[1],
283
+      A[1],
284
+      1,
285
+      B[0] * B[0] + B[1] * B[1],
286
+      B[1],
287
+      1,
288
+      C[0] * C[0] + C[1] * C[1],
289
+      C[1],
290
+      1
291
+    )
292
+    const by = Utils.det(
293
+      A[0] * A[0] + A[1] * A[1],
294
+      A[0],
295
+      1,
296
+      B[0] * B[0] + B[1] * B[1],
297
+      B[0],
298
+      1,
299
+      C[0] * C[0] + C[1] * C[1],
300
+      C[0],
301
+      1
302
+    )
303
+    const c = -Utils.det(
304
+      A[0] * A[0] + A[1] * A[1],
305
+      A[0],
306
+      A[1],
307
+      B[0] * B[0] + B[1] * B[1],
308
+      B[0],
309
+      B[1],
310
+      C[0] * C[0] + C[1] * C[1],
311
+      C[0],
312
+      C[1]
313
+    )
314
+
315
+    const x = -bx / (2 * a)
316
+    const y = -by / (2 * a)
317
+    const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
318
+
319
+    return [x, y, r]
320
+  }
321
+
322
+  /**
323
+   * Find the approximate perimeter of an ellipse.
324
+   * @param rx
325
+   * @param ry
326
+   */
327
+  static perimeterOfEllipse(rx: number, ry: number): number {
328
+    const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
329
+    const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
330
+    return p
331
+  }
332
+
333
+  /**
334
+   * Get the short angle distance between two angles.
335
+   * @param a0
336
+   * @param a1
337
+   */
59 338
   static shortAngleDist(a0: number, a1: number): number {
60 339
     const max = Math.PI * 2
61 340
     const da = (a1 - a0) % max
62 341
     return ((2 * da) % max) - da
63 342
   }
64 343
 
344
+  /**
345
+   * Get the long angle distance between two angles.
346
+   * @param a0
347
+   * @param a1
348
+   */
349
+  static longAngleDist(a0: number, a1: number): number {
350
+    return Math.PI * 2 - Utils.shortAngleDist(a0, a1)
351
+  }
352
+
353
+  /**
354
+   * Interpolate an angle between two angles.
355
+   * @param a0
356
+   * @param a1
357
+   * @param t
358
+   */
359
+  static lerpAngles(a0: number, a1: number, t: number): number {
360
+    return a0 + Utils.shortAngleDist(a0, a1) * t
361
+  }
362
+
363
+  /**
364
+   * Get the short distance between two angles.
365
+   * @param a0
366
+   * @param a1
367
+   */
65 368
   static angleDelta(a0: number, a1: number): number {
66
-    return this.shortAngleDist(a0, a1)
369
+    return Utils.shortAngleDist(a0, a1)
67 370
   }
68 371
 
372
+  /**
373
+   * Get the "sweep" or short distance between two points on a circle's perimeter.
374
+   * @param C
375
+   * @param A
376
+   * @param B
377
+   */
69 378
   static getSweep(C: number[], A: number[], B: number[]): number {
70
-    return this.angleDelta(vec.angle(C, A), vec.angle(C, B))
379
+    return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B))
380
+  }
381
+
382
+  /**
383
+   * Rotate a point around a center.
384
+   * @param x The x-axis coordinate of the point.
385
+   * @param y The y-axis coordinate of the point.
386
+   * @param cx The x-axis coordinate of the point to rotate round.
387
+   * @param cy The y-axis coordinate of the point to rotate round.
388
+   * @param angle The distance (in radians) to rotate.
389
+   */
390
+  static rotatePoint(A: number[], B: number[], angle: number): number[] {
391
+    const s = Math.sin(angle)
392
+    const c = Math.cos(angle)
393
+
394
+    const px = A[0] - B[0]
395
+    const py = A[1] - B[1]
396
+
397
+    const nx = px * c - py * s
398
+    const ny = px * s + py * c
399
+
400
+    return [nx + B[0], ny + B[1]]
401
+  }
402
+
403
+  /**
404
+   * Clamp radians within 0 and 2PI
405
+   * @param r
406
+   */
407
+  static clampRadians(r: number): number {
408
+    return (Math.PI * 2 + r) % (Math.PI * 2)
409
+  }
410
+
411
+  /**
412
+   * Clamp rotation to even segments.
413
+   * @param r
414
+   * @param segments
415
+   */
416
+  static clampToRotationToSegments(r: number, segments: number): number {
417
+    const seg = (Math.PI * 2) / segments
418
+    return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg
419
+  }
420
+
421
+  /**
422
+   * Is angle c between angles a and b?
423
+   * @param a
424
+   * @param b
425
+   * @param c
426
+   */
427
+  static isAngleBetween(a: number, b: number, c: number): boolean {
428
+    if (c === a || c === b) return true
429
+    const PI2 = Math.PI * 2
430
+    const AB = (b - a + PI2) % PI2
431
+    const AC = (c - a + PI2) % PI2
432
+    return AB <= Math.PI !== AC > AB
433
+  }
434
+
435
+  /**
436
+   * Convert degrees to radians.
437
+   * @param d
438
+   */
439
+  static degreesToRadians(d: number): number {
440
+    return (d * Math.PI) / 180
441
+  }
442
+
443
+  /**
444
+   * Convert radians to degrees.
445
+   * @param r
446
+   */
447
+  static radiansToDegrees(r: number): number {
448
+    return (r * 180) / Math.PI
449
+  }
450
+
451
+  /**
452
+   * Get the length of an arc between two points on a circle's perimeter.
453
+   * @param C
454
+   * @param r
455
+   * @param A
456
+   * @param B
457
+   */
458
+  static getArcLength(
459
+    C: number[],
460
+    r: number,
461
+    A: number[],
462
+    B: number[]
463
+  ): number {
464
+    const sweep = Utils.getSweep(C, A, B)
465
+    return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
466
+  }
467
+
468
+  /**
469
+   * Get a dash offset for an arc, based on its length.
470
+   * @param C
471
+   * @param r
472
+   * @param A
473
+   * @param B
474
+   * @param step
475
+   */
476
+  static getArcDashOffset(
477
+    C: number[],
478
+    r: number,
479
+    A: number[],
480
+    B: number[],
481
+    step: number
482
+  ): number {
483
+    const del0 = Utils.getSweep(C, A, B)
484
+    const len0 = Utils.getArcLength(C, r, A, B)
485
+    const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
486
+    return -off0 / 2 + step
487
+  }
488
+
489
+  /**
490
+   * Get a dash offset for an ellipse, based on its length.
491
+   * @param A
492
+   * @param step
493
+   */
494
+  static getEllipseDashOffset(A: number[], step: number): number {
495
+    const c = 2 * Math.PI * A[2]
496
+    return -c / 2 + -step
497
+  }
498
+
499
+  static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
500
+    return Array.from(Array(steps))
501
+      .map((_, i) => {
502
+        const t = i / steps
503
+        return t * t * t
504
+      })
505
+      .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
506
+  }
507
+
508
+  static getRayRayIntersection(
509
+    p0: number[],
510
+    n0: number[],
511
+    p1: number[],
512
+    n1: number[]
513
+  ): number[] {
514
+    const p0e = vec.add(p0, n0),
515
+      p1e = vec.add(p1, n1),
516
+      m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]),
517
+      m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]),
518
+      b0 = p0[1] - m0 * p0[0],
519
+      b1 = p1[1] - m1 * p1[0],
520
+      x = (b1 - b0) / (m0 - m1),
521
+      y = m0 * x + b0
522
+
523
+    return [x, y]
71 524
   }
72 525
 
73 526
   static bez1d(a: number, b: number, c: number, d: number, t: number): number {
@@ -167,4 +620,120 @@ export default class Utils {
167 620
 
168 621
     return bounds
169 622
   }
623
+
624
+  /**
625
+   * Get a bezier curve data for a spline that fits an array of points.
626
+   * @param pts
627
+   * @param tension
628
+   * @param isClosed
629
+   * @param numOfSegments
630
+   */
631
+  static getCurvePoints(
632
+    pts: number[][],
633
+    tension = 0.5,
634
+    isClosed = false,
635
+    numOfSegments = 3
636
+  ): number[][] {
637
+    const _pts = [...pts],
638
+      len = pts.length,
639
+      res: number[][] = [] // results
640
+
641
+    let t1x: number, // tension vectors
642
+      t2x: number,
643
+      t1y: number,
644
+      t2y: number,
645
+      c1: number, // cardinal points
646
+      c2: number,
647
+      c3: number,
648
+      c4: number,
649
+      st: number,
650
+      st2: number,
651
+      st3: number
652
+
653
+    // The algorithm require a previous and next point to the actual point array.
654
+    // Check if we will draw closed or open curve.
655
+    // If closed, copy end points to beginning and first points to end
656
+    // If open, duplicate first points to befinning, end points to end
657
+    if (isClosed) {
658
+      _pts.unshift(_pts[len - 1])
659
+      _pts.push(_pts[0])
660
+    } else {
661
+      //copy 1. point and insert at beginning
662
+      _pts.unshift(_pts[0])
663
+      _pts.push(_pts[len - 1])
664
+      // _pts.push(_pts[len - 1])
665
+    }
666
+
667
+    // For each point, calculate a segment
668
+    for (let i = 1; i < _pts.length - 2; i++) {
669
+      // Calculate points along segment and add to results
670
+      for (let t = 0; t <= numOfSegments; t++) {
671
+        // Step
672
+        st = t / numOfSegments
673
+        st2 = Math.pow(st, 2)
674
+        st3 = Math.pow(st, 3)
675
+
676
+        // Cardinals
677
+        c1 = 2 * st3 - 3 * st2 + 1
678
+        c2 = -(2 * st3) + 3 * st2
679
+        c3 = st3 - 2 * st2 + st
680
+        c4 = st3 - st2
681
+
682
+        // Tension
683
+        t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
684
+        t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
685
+        t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
686
+        t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
687
+
688
+        // Control points
689
+        res.push([
690
+          c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
691
+          c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
692
+        ])
693
+      }
694
+    }
695
+
696
+    res.push(pts[pts.length - 1])
697
+
698
+    return res
699
+  }
700
+
701
+  /**
702
+   * Simplify a line (using Ramer-Douglas-Peucker algorithm).
703
+   * @param points An array of points as [x, y, ...][]
704
+   * @param tolerance The minimum line distance (also called epsilon).
705
+   * @returns Simplified array as [x, y, ...][]
706
+   */
707
+  static simplify(points: number[][], tolerance = 1): number[][] {
708
+    const len = points.length,
709
+      a = points[0],
710
+      b = points[len - 1],
711
+      [x1, y1] = a,
712
+      [x2, y2] = b
713
+
714
+    if (len > 2) {
715
+      let distance = 0
716
+      let index = 0
717
+      const max = Math.hypot(y2 - y1, x2 - x1)
718
+
719
+      for (let i = 1; i < len - 1; i++) {
720
+        const [x0, y0] = points[i],
721
+          d =
722
+            Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
723
+
724
+        if (distance > d) continue
725
+
726
+        distance = d
727
+        index = i
728
+      }
729
+
730
+      if (distance > tolerance) {
731
+        const l0 = Utils.simplify(points.slice(0, index + 1), tolerance)
732
+        const l1 = Utils.simplify(points.slice(index + 1), tolerance)
733
+        return l0.concat(l1.slice(1))
734
+      }
735
+    }
736
+
737
+    return [a, b]
738
+  }
170 739
 }

+ 2
- 4
types.ts View File

@@ -191,12 +191,10 @@ export interface GroupShape extends BaseShape {
191 191
   size: number[]
192 192
 }
193 193
 
194
-type DeepPartial<T> = {
195
-  [P in keyof T]?: DeepPartial<T[P]>
194
+export type ShapeProps<T extends Shape> = Partial<T> & {
195
+  style?: Partial<ShapeStyles>
196 196
 }
197 197
 
198
-export type ShapeProps<T extends Shape> = DeepPartial<T>
199
-
200 198
 export type MutableShape =
201 199
   | DotShape
202 200
   | EllipseShape

+ 10
- 10
utils/utils.ts View File

@@ -198,16 +198,6 @@ export function setToArray<T>(set: Set<T>): T[] {
198 198
 
199 199
 /* -------------------- Hit Tests ------------------- */
200 200
 
201
-/**
202
- * Get whether a point is inside of a bounds.
203
- * @param A
204
- * @param b
205
- * @returns
206
- */
207
-export function pointInBounds(A: number[], b: Bounds): boolean {
208
-  return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
209
-}
210
-
211 201
 /**
212 202
  * Get whether a point is inside of a circle.
213 203
  * @param A
@@ -265,6 +255,16 @@ export function pointInRect(
265 255
 
266 256
 /* --------------------- Bounds --------------------- */
267 257
 
258
+/**
259
+ * Get whether a point is inside of a bounds.
260
+ * @param A
261
+ * @param b
262
+ * @returns
263
+ */
264
+export function pointInBounds(A: number[], b: Bounds): boolean {
265
+  return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
266
+}
267
+
268 268
 /**
269 269
  * Get whether two bounds collide.
270 270
  * @param a Bounds

+ 35
- 1
yarn.lock View File

@@ -3073,6 +3073,11 @@ commander@^2.20.0:
3073 3073
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
3074 3074
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
3075 3075
 
3076
+commander@^4.0.0:
3077
+  version "4.1.1"
3078
+  resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
3079
+  integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
3080
+
3076 3081
 commander@^6.2.0:
3077 3082
   version "6.2.1"
3078 3083
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
@@ -4141,6 +4146,18 @@ glob-to-regexp@^0.4.1:
4141 4146
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
4142 4147
   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
4143 4148
 
4149
+glob@7.1.6:
4150
+  version "7.1.6"
4151
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
4152
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
4153
+  dependencies:
4154
+    fs.realpath "^1.0.0"
4155
+    inflight "^1.0.4"
4156
+    inherits "2"
4157
+    minimatch "^3.0.4"
4158
+    once "^1.3.0"
4159
+    path-is-absolute "^1.0.0"
4160
+
4144 4161
 glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
4145 4162
   version "7.1.7"
4146 4163
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@@ -5741,7 +5758,7 @@ ms@^2.1.1:
5741 5758
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
5742 5759
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
5743 5760
 
5744
-mz@^2.4.0:
5761
+mz@^2.4.0, mz@^2.7.0:
5745 5762
   version "2.7.0"
5746 5763
   resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
5747 5764
   integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
@@ -7372,6 +7389,18 @@ stylis@3.5.4:
7372 7389
   resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
7373 7390
   integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
7374 7391
 
7392
+sucrase@^3.19.0:
7393
+  version "3.19.0"
7394
+  resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.19.0.tgz#cc9a60f731e7497766a7b710d3362260a8f9ced5"
7395
+  integrity sha512-FeMelydANPRMiOo/lxbf7NxN8bQmMVBQmKOa69BifwVhteMJzRoJNHaVBoCYmE/kpnx6VPg9ckaLumwtuAzmEA==
7396
+  dependencies:
7397
+    commander "^4.0.0"
7398
+    glob "7.1.6"
7399
+    lines-and-columns "^1.1.6"
7400
+    mz "^2.7.0"
7401
+    pirates "^4.0.1"
7402
+    ts-interface-checker "^0.1.9"
7403
+
7375 7404
 supports-color@^2.0.0:
7376 7405
   version "2.0.0"
7377 7406
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -7562,6 +7591,11 @@ tr46@^2.1.0:
7562 7591
   dependencies:
7563 7592
     punycode "^2.1.1"
7564 7593
 
7594
+ts-interface-checker@^0.1.9:
7595
+  version "0.1.13"
7596
+  resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
7597
+  integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
7598
+
7565 7599
 ts-pnp@^1.1.6:
7566 7600
   version "1.2.0"
7567 7601
   resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"

Loading…
Cancel
Save