kopia lustrzana https://github.com/Tldraw/Tldraw
Updates to code editor, utils
rodzic
cbc26f2e06
commit
69bdab520a
|
@ -81,20 +81,29 @@ export default function CodeEditor({
|
|||
|
||||
monaco.languages.registerDocumentFormattingEditProvider('typescript', {
|
||||
async provideDocumentFormattingEdits(model) {
|
||||
const text = prettier.format(model.getValue(), {
|
||||
parser: 'typescript',
|
||||
plugins: [parserTypeScript],
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
semi: false,
|
||||
})
|
||||
try {
|
||||
const text = prettier.format(model.getValue(), {
|
||||
parser: 'typescript',
|
||||
plugins: [parserTypeScript],
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
semi: false,
|
||||
})
|
||||
|
||||
return [
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text,
|
||||
},
|
||||
]
|
||||
return [
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text,
|
||||
},
|
||||
]
|
||||
} catch (e) {
|
||||
return [
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text: model.getValue(),
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
|
|
@ -5,7 +5,6 @@ import React, { useEffect, useRef } from 'react'
|
|||
import state, { useSelector } from 'state'
|
||||
import { CodeFile } from 'types'
|
||||
import CodeDocs from './code-docs'
|
||||
import CodeEditor from './code-editor'
|
||||
import { generateFromCode } from 'state/code/generate'
|
||||
import * as Panel from '../panel'
|
||||
import { IconButton } from '../shared'
|
||||
|
@ -17,6 +16,8 @@ import {
|
|||
ChevronUp,
|
||||
ChevronDown,
|
||||
} from 'react-feather'
|
||||
import dynamic from 'next/dynamic'
|
||||
const CodeEditor = dynamic(() => import('./code-editor'))
|
||||
|
||||
const getErrorLineAndColumn = (e: any) => {
|
||||
if ('line' in e) {
|
||||
|
@ -84,7 +85,7 @@ export default function CodePanel(): JSX.Element {
|
|||
const { shapes, controls } = generateFromCode(state.data, data.code)
|
||||
state.send('GENERATED_FROM_CODE', { shapes, controls })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error('Got an error!', e)
|
||||
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||
}
|
||||
|
||||
|
|
|
@ -199,12 +199,10 @@ interface GroupShape extends BaseShape {
|
|||
size: number[]
|
||||
}
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>
|
||||
type ShapeProps<T extends Shape> = Partial<T> & {
|
||||
style?: Partial<ShapeStyles>
|
||||
}
|
||||
|
||||
type ShapeProps<T extends Shape> = DeepPartial<T>
|
||||
|
||||
type MutableShape =
|
||||
| DotShape
|
||||
| EllipseShape
|
||||
|
@ -817,7 +815,7 @@ interface ShapeUtility<K extends Shape> {
|
|||
* ## Rectangle
|
||||
*/
|
||||
class Rectangle extends CodeShape<RectangleShape> {
|
||||
constructor(props = {} as Partial<RectangleShape> & Partial<ShapeStyles>) {
|
||||
constructor(props = {} as ShapeProps<RectangleShape>) {
|
||||
super({
|
||||
id: uniqueId(),
|
||||
seed: Math.random(),
|
||||
|
@ -970,6 +968,497 @@ interface ShapeUtility<K extends Shape> {
|
|||
* ## Utils
|
||||
*/
|
||||
class Utils {
|
||||
/**
|
||||
* Linear interpolation betwen two numbers.
|
||||
* @param y1
|
||||
* @param y2
|
||||
* @param mu
|
||||
*/
|
||||
static lerp(y1: number, y2: number, mu: number): number {
|
||||
mu = Utils.clamp(mu, 0, 1)
|
||||
return y1 * (1 - mu) + y2 * mu
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulate a value between two ranges.
|
||||
* @param value
|
||||
* @param rangeA from [low, high]
|
||||
* @param rangeB to [low, high]
|
||||
* @param clamp
|
||||
*/
|
||||
static modulate(
|
||||
value: number,
|
||||
rangeA: number[],
|
||||
rangeB: number[],
|
||||
clamp = false
|
||||
): number {
|
||||
const [fromLow, fromHigh] = rangeA
|
||||
const [v0, v1] = rangeB
|
||||
const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
|
||||
|
||||
return clamp
|
||||
? v0 < v1
|
||||
? Math.max(Math.min(result, v1), v0)
|
||||
: Math.max(Math.min(result, v0), v1)
|
||||
: result
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clamp(n: number, min: number): number
|
||||
static clamp(n: number, min: number, max: number): number
|
||||
static clamp(n: number, min: number, max?: number): number {
|
||||
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||
}
|
||||
|
||||
// TODO: replace with a string compression algorithm
|
||||
static compress(s: string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO: replace with a string decompression algorithm
|
||||
static decompress(s: string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clone an object or array.
|
||||
* @param obj
|
||||
*/
|
||||
static deepClone<T>(obj: T): T {
|
||||
if (obj === null) return null
|
||||
|
||||
const clone: any = { ...obj }
|
||||
|
||||
Object.keys(obj).forEach(
|
||||
(key) =>
|
||||
(clone[key] =
|
||||
typeof obj[key] === 'object' ? Utils.deepClone(obj[key]) : obj[key])
|
||||
)
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
clone.length = obj.length
|
||||
return Array.from(clone) as any as T
|
||||
}
|
||||
|
||||
return clone as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift).
|
||||
* The result will always be betweeen -1 and 1.
|
||||
*
|
||||
* Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
|
||||
*/
|
||||
static rng(seed = ''): () => number {
|
||||
let x = 0
|
||||
let y = 0
|
||||
let z = 0
|
||||
let w = 0
|
||||
|
||||
function next() {
|
||||
const t = x ^ (x << 11)
|
||||
;(x = y), (y = z), (z = w)
|
||||
w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0
|
||||
return w / 0x100000000
|
||||
}
|
||||
|
||||
for (let k = 0; k < seed.length + 64; k++) {
|
||||
;(x ^= seed.charCodeAt(k) | 0), next()
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle the contents of an array.
|
||||
* @param arr
|
||||
* @param offset
|
||||
*/
|
||||
static shuffleArr<T>(arr: T[], offset: number): T[] {
|
||||
return arr.map((_, i) => arr[(i + offset) % arr.length])
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare two arrays.
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
static deepCompareArrays<T>(a: T[], b: T[]): boolean {
|
||||
if (a?.length !== b?.length) return false
|
||||
return Utils.deepCompare(a, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare any values.
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
static deepCompare<T>(a: T, b: T): boolean {
|
||||
return a === b || JSON.stringify(a) === JSON.stringify(b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find whether two arrays intersect.
|
||||
* @param a
|
||||
* @param b
|
||||
* @param fn An optional function to apply to the items of a; will check if b includes the result.
|
||||
*/
|
||||
static arrsIntersect<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
|
||||
static arrsIntersect<T>(a: T[], b: T[]): boolean
|
||||
static arrsIntersect<T>(
|
||||
a: T[],
|
||||
b: unknown[],
|
||||
fn?: (item: unknown) => T
|
||||
): boolean {
|
||||
return a.some((item) => b.includes(fn ? fn(item) : item))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique values from an array of strings or numbers.
|
||||
* @param items
|
||||
*/
|
||||
static uniqueArray<T extends string | number>(...items: T[]): T[] {
|
||||
return Array.from(new Set(items).values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a set to an array.
|
||||
* @param set
|
||||
*/
|
||||
static setToArray<T>(set: Set<T>): T[] {
|
||||
return Array.from(set.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outer of between a circle and a point.
|
||||
* @param C The circle's center.
|
||||
* @param r The circle's radius.
|
||||
* @param P The point.
|
||||
* @param side
|
||||
*/
|
||||
static getCircleTangentToPoint(
|
||||
C: number[],
|
||||
r: number,
|
||||
P: number[],
|
||||
side: number
|
||||
): number[] {
|
||||
const B = vec.lrp(C, P, 0.5),
|
||||
r1 = vec.dist(C, B),
|
||||
delta = vec.sub(B, C),
|
||||
d = vec.len(delta)
|
||||
|
||||
if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = (r * r - r1 * r1 + d * d) / (2.0 * d),
|
||||
n = 1 / d,
|
||||
p = vec.add(C, vec.mul(delta, a * n)),
|
||||
h = Math.sqrt(r * r - a * a),
|
||||
k = vec.mul(vec.per(delta), h * n)
|
||||
|
||||
return side === 0 ? vec.add(p, k) : vec.sub(p, k)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outer tangents of two circles.
|
||||
* @param x0
|
||||
* @param y0
|
||||
* @param r0
|
||||
* @param x1
|
||||
* @param y1
|
||||
* @param r1
|
||||
* @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
|
||||
*/
|
||||
static getOuterTangentsOfCircles(
|
||||
C0: number[],
|
||||
r0: number,
|
||||
C1: number[],
|
||||
r1: number
|
||||
): number[][] {
|
||||
const a0 = vec.angle(C0, C1)
|
||||
const d = vec.dist(C0, C1)
|
||||
|
||||
// Circles are overlapping, no tangents
|
||||
if (d < Math.abs(r1 - r0)) return
|
||||
|
||||
const a1 = Math.acos((r0 - r1) / d),
|
||||
t0 = a0 + a1,
|
||||
t1 = a0 - a1
|
||||
|
||||
return [
|
||||
[C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
|
||||
[C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
|
||||
[C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
|
||||
[C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the closest point on the perimeter of a circle to a given point.
|
||||
* @param C The circle's center.
|
||||
* @param r The circle's radius.
|
||||
* @param P The point.
|
||||
*/
|
||||
static getClosestPointOnCircle(
|
||||
C: number[],
|
||||
r: number,
|
||||
P: number[]
|
||||
): number[] {
|
||||
const v = vec.sub(C, P)
|
||||
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
|
||||
}
|
||||
|
||||
static det(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number,
|
||||
g: number,
|
||||
h: number,
|
||||
i: number
|
||||
): number {
|
||||
return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a circle from three points.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param C
|
||||
* @returns [x, y, r]
|
||||
*/
|
||||
static circleFromThreePoints(
|
||||
A: number[],
|
||||
B: number[],
|
||||
C: number[]
|
||||
): number[] {
|
||||
const a = Utils.det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
|
||||
|
||||
const bx = -Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[1],
|
||||
1,
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[1],
|
||||
1,
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[1],
|
||||
1
|
||||
)
|
||||
const by = Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[0],
|
||||
1,
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[0],
|
||||
1,
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[0],
|
||||
1
|
||||
)
|
||||
const c = -Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[0],
|
||||
A[1],
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[0],
|
||||
B[1],
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[0],
|
||||
C[1]
|
||||
)
|
||||
|
||||
const x = -bx / (2 * a)
|
||||
const y = -by / (2 * a)
|
||||
const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
|
||||
|
||||
return [x, y, r]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the approximate perimeter of an ellipse.
|
||||
* @param rx
|
||||
* @param ry
|
||||
*/
|
||||
static perimeterOfEllipse(rx: number, ry: number): number {
|
||||
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
||||
const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short angle distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static shortAngleDist(a0: number, a1: number): number {
|
||||
const max = Math.PI * 2
|
||||
const da = (a1 - a0) % max
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the long angle distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static longAngleDist(a0: number, a1: number): number {
|
||||
return Math.PI * 2 - Utils.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate an angle between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
* @param t
|
||||
*/
|
||||
static lerpAngles(a0: number, a1: number, t: number): number {
|
||||
return a0 + Utils.shortAngleDist(a0, a1) * t
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static angleDelta(a0: number, a1: number): number {
|
||||
return Utils.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "sweep" or short distance between two points on a circle's perimeter.
|
||||
* @param C
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static getSweep(C: number[], A: number[], B: number[]): number {
|
||||
return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B))
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a point around a center.
|
||||
* @param x The x-axis coordinate of the point.
|
||||
* @param y The y-axis coordinate of the point.
|
||||
* @param cx The x-axis coordinate of the point to rotate round.
|
||||
* @param cy The y-axis coordinate of the point to rotate round.
|
||||
* @param angle The distance (in radians) to rotate.
|
||||
*/
|
||||
static rotatePoint(A: number[], B: number[], angle: number): number[] {
|
||||
const s = Math.sin(angle)
|
||||
const c = Math.cos(angle)
|
||||
|
||||
const px = A[0] - B[0]
|
||||
const py = A[1] - B[1]
|
||||
|
||||
const nx = px * c - py * s
|
||||
const ny = px * s + py * c
|
||||
|
||||
return [nx + B[0], ny + B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp radians within 0 and 2PI
|
||||
* @param r
|
||||
*/
|
||||
static clampRadians(r: number): number {
|
||||
return (Math.PI * 2 + r) % (Math.PI * 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp rotation to even segments.
|
||||
* @param r
|
||||
* @param segments
|
||||
*/
|
||||
static clampToRotationToSegments(r: number, segments: number): number {
|
||||
const seg = (Math.PI * 2) / segments
|
||||
return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg
|
||||
}
|
||||
|
||||
/**
|
||||
* Is angle c between angles a and b?
|
||||
* @param a
|
||||
* @param b
|
||||
* @param c
|
||||
*/
|
||||
static isAngleBetween(a: number, b: number, c: number): boolean {
|
||||
if (c === a || c === b) return true
|
||||
const PI2 = Math.PI * 2
|
||||
const AB = (b - a + PI2) % PI2
|
||||
const AC = (c - a + PI2) % PI2
|
||||
return AB <= Math.PI !== AC > AB
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians.
|
||||
* @param d
|
||||
*/
|
||||
static degreesToRadians(d: number): number {
|
||||
return (d * Math.PI) / 180
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert radians to degrees.
|
||||
* @param r
|
||||
*/
|
||||
static radiansToDegrees(r: number): number {
|
||||
return (r * 180) / Math.PI
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of an arc between two points on a circle's perimeter.
|
||||
* @param C
|
||||
* @param r
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static getArcLength(
|
||||
C: number[],
|
||||
r: number,
|
||||
A: number[],
|
||||
B: number[]
|
||||
): number {
|
||||
const sweep = Utils.getSweep(C, A, B)
|
||||
return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dash offset for an arc, based on its length.
|
||||
* @param C
|
||||
* @param r
|
||||
* @param A
|
||||
* @param B
|
||||
* @param step
|
||||
*/
|
||||
static getArcDashOffset(
|
||||
C: number[],
|
||||
r: number,
|
||||
A: number[],
|
||||
B: number[],
|
||||
step: number
|
||||
): number {
|
||||
const del0 = Utils.getSweep(C, A, B)
|
||||
const len0 = Utils.getArcLength(C, r, A, B)
|
||||
const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
|
||||
return -off0 / 2 + step
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dash offset for an ellipse, based on its length.
|
||||
* @param A
|
||||
* @param step
|
||||
*/
|
||||
static getEllipseDashOffset(A: number[], step: number): number {
|
||||
const c = 2 * Math.PI * A[2]
|
||||
return -c / 2 + -step
|
||||
}
|
||||
|
||||
static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
|
||||
return Array.from(Array(steps))
|
||||
.map((_, i) => {
|
||||
|
@ -997,44 +1486,6 @@ interface ShapeUtility<K extends Shape> {
|
|||
return [x, y]
|
||||
}
|
||||
|
||||
static getCircleTangentToPoint(
|
||||
A: number[],
|
||||
r0: number,
|
||||
P: number[],
|
||||
side: number
|
||||
): number[] {
|
||||
const B = vec.lrp(A, P, 0.5),
|
||||
r1 = vec.dist(A, B),
|
||||
delta = vec.sub(B, A),
|
||||
d = vec.len(delta)
|
||||
|
||||
if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
|
||||
n = 1 / d,
|
||||
p = vec.add(A, vec.mul(delta, a * n)),
|
||||
h = Math.sqrt(r0 * r0 - a * a),
|
||||
k = vec.mul(vec.per(delta), h * n)
|
||||
|
||||
return side === 0 ? vec.add(p, k) : vec.sub(p, k)
|
||||
}
|
||||
|
||||
static shortAngleDist(a0: number, a1: number): number {
|
||||
const max = Math.PI * 2
|
||||
const da = (a1 - a0) % max
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
static angleDelta(a0: number, a1: number): number {
|
||||
return this.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
static getSweep(C: number[], A: number[], B: number[]): number {
|
||||
return this.angleDelta(vec.angle(C, A), vec.angle(C, B))
|
||||
}
|
||||
|
||||
static bez1d(a: number, b: number, c: number, d: number, t: number): number {
|
||||
return (
|
||||
a * (1 - t) * (1 - t) * (1 - t) +
|
||||
|
@ -1132,6 +1583,122 @@ interface ShapeUtility<K extends Shape> {
|
|||
|
||||
return bounds
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bezier curve data for a spline that fits an array of points.
|
||||
* @param pts
|
||||
* @param tension
|
||||
* @param isClosed
|
||||
* @param numOfSegments
|
||||
*/
|
||||
static getCurvePoints(
|
||||
pts: number[][],
|
||||
tension = 0.5,
|
||||
isClosed = false,
|
||||
numOfSegments = 3
|
||||
): number[][] {
|
||||
const _pts = [...pts],
|
||||
len = pts.length,
|
||||
res: number[][] = [] // results
|
||||
|
||||
let t1x: number, // tension vectors
|
||||
t2x: number,
|
||||
t1y: number,
|
||||
t2y: number,
|
||||
c1: number, // cardinal points
|
||||
c2: number,
|
||||
c3: number,
|
||||
c4: number,
|
||||
st: number,
|
||||
st2: number,
|
||||
st3: number
|
||||
|
||||
// The algorithm require a previous and next point to the actual point array.
|
||||
// Check if we will draw closed or open curve.
|
||||
// If closed, copy end points to beginning and first points to end
|
||||
// If open, duplicate first points to befinning, end points to end
|
||||
if (isClosed) {
|
||||
_pts.unshift(_pts[len - 1])
|
||||
_pts.push(_pts[0])
|
||||
} else {
|
||||
//copy 1. point and insert at beginning
|
||||
_pts.unshift(_pts[0])
|
||||
_pts.push(_pts[len - 1])
|
||||
// _pts.push(_pts[len - 1])
|
||||
}
|
||||
|
||||
// For each point, calculate a segment
|
||||
for (let i = 1; i < _pts.length - 2; i++) {
|
||||
// Calculate points along segment and add to results
|
||||
for (let t = 0; t <= numOfSegments; t++) {
|
||||
// Step
|
||||
st = t / numOfSegments
|
||||
st2 = Math.pow(st, 2)
|
||||
st3 = Math.pow(st, 3)
|
||||
|
||||
// Cardinals
|
||||
c1 = 2 * st3 - 3 * st2 + 1
|
||||
c2 = -(2 * st3) + 3 * st2
|
||||
c3 = st3 - 2 * st2 + st
|
||||
c4 = st3 - st2
|
||||
|
||||
// Tension
|
||||
t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
|
||||
t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
|
||||
t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
|
||||
t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
|
||||
|
||||
// Control points
|
||||
res.push([
|
||||
c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
|
||||
c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
res.push(pts[pts.length - 1])
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify a line (using Ramer-Douglas-Peucker algorithm).
|
||||
* @param points An array of points as [x, y, ...][]
|
||||
* @param tolerance The minimum line distance (also called epsilon).
|
||||
* @returns Simplified array as [x, y, ...][]
|
||||
*/
|
||||
static simplify(points: number[][], tolerance = 1): number[][] {
|
||||
const len = points.length,
|
||||
a = points[0],
|
||||
b = points[len - 1],
|
||||
[x1, y1] = a,
|
||||
[x2, y2] = b
|
||||
|
||||
if (len > 2) {
|
||||
let distance = 0
|
||||
let index = 0
|
||||
const max = Math.hypot(y2 - y1, x2 - x1)
|
||||
|
||||
for (let i = 1; i < len - 1; i++) {
|
||||
const [x0, y0] = points[i],
|
||||
d =
|
||||
Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
|
||||
|
||||
if (distance > d) continue
|
||||
|
||||
distance = d
|
||||
index = i
|
||||
}
|
||||
|
||||
if (distance > tolerance) {
|
||||
const l0 = Utils.simplify(points.slice(0, index + 1), tolerance)
|
||||
const l1 = Utils.simplify(points.slice(index + 1), tolerance)
|
||||
return l0.concat(l1.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
|
||||
// A big collection of vector utilities. Collected into a class to improve logging / packaging.
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-feather": "^2.0.9",
|
||||
"react-use-gesture": "^9.1.3",
|
||||
"sucrase": "^3.19.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { NumberControl, VectorControl, codeControls, controls } from './control'
|
|||
import { codeShapes } from './index'
|
||||
import { CodeControl, Data, Shape } from 'types'
|
||||
import { getPage } from 'utils'
|
||||
import { transform } from 'sucrase'
|
||||
|
||||
const baseScope = {
|
||||
Dot,
|
||||
|
@ -48,7 +49,9 @@ export function generateFromCode(
|
|||
const { currentPageId } = data
|
||||
const scope = { ...baseScope, controls, currentPageId }
|
||||
|
||||
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
||||
const transformed = transform(code, { transforms: ['typescript'] }).code
|
||||
|
||||
new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope))
|
||||
|
||||
const generatedShapes = Array.from(codeShapes.values()).map((instance) => ({
|
||||
...instance.shape,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { RectangleShape, ShapeStyles, ShapeType } from 'types'
|
||||
import { RectangleShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
/**
|
||||
* ## Rectangle
|
||||
*/
|
||||
export default class Rectangle extends CodeShape<RectangleShape> {
|
||||
constructor(props = {} as Partial<RectangleShape> & Partial<ShapeStyles>) {
|
||||
constructor(props = {} as ShapeProps<RectangleShape>) {
|
||||
super({
|
||||
id: uniqueId(),
|
||||
seed: Math.random(),
|
||||
|
|
|
@ -5,6 +5,497 @@ import vec from 'utils/vec'
|
|||
* ## Utils
|
||||
*/
|
||||
export default class Utils {
|
||||
/**
|
||||
* Linear interpolation betwen two numbers.
|
||||
* @param y1
|
||||
* @param y2
|
||||
* @param mu
|
||||
*/
|
||||
static lerp(y1: number, y2: number, mu: number): number {
|
||||
mu = Utils.clamp(mu, 0, 1)
|
||||
return y1 * (1 - mu) + y2 * mu
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulate a value between two ranges.
|
||||
* @param value
|
||||
* @param rangeA from [low, high]
|
||||
* @param rangeB to [low, high]
|
||||
* @param clamp
|
||||
*/
|
||||
static modulate(
|
||||
value: number,
|
||||
rangeA: number[],
|
||||
rangeB: number[],
|
||||
clamp = false
|
||||
): number {
|
||||
const [fromLow, fromHigh] = rangeA
|
||||
const [v0, v1] = rangeB
|
||||
const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
|
||||
|
||||
return clamp
|
||||
? v0 < v1
|
||||
? Math.max(Math.min(result, v1), v0)
|
||||
: Math.max(Math.min(result, v0), v1)
|
||||
: result
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clamp(n: number, min: number): number
|
||||
static clamp(n: number, min: number, max: number): number
|
||||
static clamp(n: number, min: number, max?: number): number {
|
||||
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||
}
|
||||
|
||||
// TODO: replace with a string compression algorithm
|
||||
static compress(s: string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO: replace with a string decompression algorithm
|
||||
static decompress(s: string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clone an object or array.
|
||||
* @param obj
|
||||
*/
|
||||
static deepClone<T>(obj: T): T {
|
||||
if (obj === null) return null
|
||||
|
||||
const clone: any = { ...obj }
|
||||
|
||||
Object.keys(obj).forEach(
|
||||
(key) =>
|
||||
(clone[key] =
|
||||
typeof obj[key] === 'object' ? Utils.deepClone(obj[key]) : obj[key])
|
||||
)
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
clone.length = obj.length
|
||||
return Array.from(clone) as any as T
|
||||
}
|
||||
|
||||
return clone as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift).
|
||||
* The result will always be betweeen -1 and 1.
|
||||
*
|
||||
* Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
|
||||
*/
|
||||
static rng(seed = ''): () => number {
|
||||
let x = 0
|
||||
let y = 0
|
||||
let z = 0
|
||||
let w = 0
|
||||
|
||||
function next() {
|
||||
const t = x ^ (x << 11)
|
||||
;(x = y), (y = z), (z = w)
|
||||
w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0
|
||||
return w / 0x100000000
|
||||
}
|
||||
|
||||
for (let k = 0; k < seed.length + 64; k++) {
|
||||
;(x ^= seed.charCodeAt(k) | 0), next()
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle the contents of an array.
|
||||
* @param arr
|
||||
* @param offset
|
||||
*/
|
||||
static shuffleArr<T>(arr: T[], offset: number): T[] {
|
||||
return arr.map((_, i) => arr[(i + offset) % arr.length])
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare two arrays.
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
static deepCompareArrays<T>(a: T[], b: T[]): boolean {
|
||||
if (a?.length !== b?.length) return false
|
||||
return Utils.deepCompare(a, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep compare any values.
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
static deepCompare<T>(a: T, b: T): boolean {
|
||||
return a === b || JSON.stringify(a) === JSON.stringify(b)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find whether two arrays intersect.
|
||||
* @param a
|
||||
* @param b
|
||||
* @param fn An optional function to apply to the items of a; will check if b includes the result.
|
||||
*/
|
||||
static arrsIntersect<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
|
||||
static arrsIntersect<T>(a: T[], b: T[]): boolean
|
||||
static arrsIntersect<T>(
|
||||
a: T[],
|
||||
b: unknown[],
|
||||
fn?: (item: unknown) => T
|
||||
): boolean {
|
||||
return a.some((item) => b.includes(fn ? fn(item) : item))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique values from an array of strings or numbers.
|
||||
* @param items
|
||||
*/
|
||||
static uniqueArray<T extends string | number>(...items: T[]): T[] {
|
||||
return Array.from(new Set(items).values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a set to an array.
|
||||
* @param set
|
||||
*/
|
||||
static setToArray<T>(set: Set<T>): T[] {
|
||||
return Array.from(set.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outer of between a circle and a point.
|
||||
* @param C The circle's center.
|
||||
* @param r The circle's radius.
|
||||
* @param P The point.
|
||||
* @param side
|
||||
*/
|
||||
static getCircleTangentToPoint(
|
||||
C: number[],
|
||||
r: number,
|
||||
P: number[],
|
||||
side: number
|
||||
): number[] {
|
||||
const B = vec.lrp(C, P, 0.5),
|
||||
r1 = vec.dist(C, B),
|
||||
delta = vec.sub(B, C),
|
||||
d = vec.len(delta)
|
||||
|
||||
if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = (r * r - r1 * r1 + d * d) / (2.0 * d),
|
||||
n = 1 / d,
|
||||
p = vec.add(C, vec.mul(delta, a * n)),
|
||||
h = Math.sqrt(r * r - a * a),
|
||||
k = vec.mul(vec.per(delta), h * n)
|
||||
|
||||
return side === 0 ? vec.add(p, k) : vec.sub(p, k)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outer tangents of two circles.
|
||||
* @param x0
|
||||
* @param y0
|
||||
* @param r0
|
||||
* @param x1
|
||||
* @param y1
|
||||
* @param r1
|
||||
* @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1]
|
||||
*/
|
||||
static getOuterTangentsOfCircles(
|
||||
C0: number[],
|
||||
r0: number,
|
||||
C1: number[],
|
||||
r1: number
|
||||
): number[][] {
|
||||
const a0 = vec.angle(C0, C1)
|
||||
const d = vec.dist(C0, C1)
|
||||
|
||||
// Circles are overlapping, no tangents
|
||||
if (d < Math.abs(r1 - r0)) return
|
||||
|
||||
const a1 = Math.acos((r0 - r1) / d),
|
||||
t0 = a0 + a1,
|
||||
t1 = a0 - a1
|
||||
|
||||
return [
|
||||
[C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)],
|
||||
[C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)],
|
||||
[C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)],
|
||||
[C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)],
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the closest point on the perimeter of a circle to a given point.
|
||||
* @param C The circle's center.
|
||||
* @param r The circle's radius.
|
||||
* @param P The point.
|
||||
*/
|
||||
static getClosestPointOnCircle(
|
||||
C: number[],
|
||||
r: number,
|
||||
P: number[]
|
||||
): number[] {
|
||||
const v = vec.sub(C, P)
|
||||
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
|
||||
}
|
||||
|
||||
static det(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number,
|
||||
g: number,
|
||||
h: number,
|
||||
i: number
|
||||
): number {
|
||||
return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a circle from three points.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param C
|
||||
* @returns [x, y, r]
|
||||
*/
|
||||
static circleFromThreePoints(
|
||||
A: number[],
|
||||
B: number[],
|
||||
C: number[]
|
||||
): number[] {
|
||||
const a = Utils.det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
|
||||
|
||||
const bx = -Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[1],
|
||||
1,
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[1],
|
||||
1,
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[1],
|
||||
1
|
||||
)
|
||||
const by = Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[0],
|
||||
1,
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[0],
|
||||
1,
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[0],
|
||||
1
|
||||
)
|
||||
const c = -Utils.det(
|
||||
A[0] * A[0] + A[1] * A[1],
|
||||
A[0],
|
||||
A[1],
|
||||
B[0] * B[0] + B[1] * B[1],
|
||||
B[0],
|
||||
B[1],
|
||||
C[0] * C[0] + C[1] * C[1],
|
||||
C[0],
|
||||
C[1]
|
||||
)
|
||||
|
||||
const x = -bx / (2 * a)
|
||||
const y = -by / (2 * a)
|
||||
const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
|
||||
|
||||
return [x, y, r]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the approximate perimeter of an ellipse.
|
||||
* @param rx
|
||||
* @param ry
|
||||
*/
|
||||
static perimeterOfEllipse(rx: number, ry: number): number {
|
||||
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
||||
const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short angle distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static shortAngleDist(a0: number, a1: number): number {
|
||||
const max = Math.PI * 2
|
||||
const da = (a1 - a0) % max
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the long angle distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static longAngleDist(a0: number, a1: number): number {
|
||||
return Math.PI * 2 - Utils.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate an angle between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
* @param t
|
||||
*/
|
||||
static lerpAngles(a0: number, a1: number, t: number): number {
|
||||
return a0 + Utils.shortAngleDist(a0, a1) * t
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short distance between two angles.
|
||||
* @param a0
|
||||
* @param a1
|
||||
*/
|
||||
static angleDelta(a0: number, a1: number): number {
|
||||
return Utils.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "sweep" or short distance between two points on a circle's perimeter.
|
||||
* @param C
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static getSweep(C: number[], A: number[], B: number[]): number {
|
||||
return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B))
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a point around a center.
|
||||
* @param x The x-axis coordinate of the point.
|
||||
* @param y The y-axis coordinate of the point.
|
||||
* @param cx The x-axis coordinate of the point to rotate round.
|
||||
* @param cy The y-axis coordinate of the point to rotate round.
|
||||
* @param angle The distance (in radians) to rotate.
|
||||
*/
|
||||
static rotatePoint(A: number[], B: number[], angle: number): number[] {
|
||||
const s = Math.sin(angle)
|
||||
const c = Math.cos(angle)
|
||||
|
||||
const px = A[0] - B[0]
|
||||
const py = A[1] - B[1]
|
||||
|
||||
const nx = px * c - py * s
|
||||
const ny = px * s + py * c
|
||||
|
||||
return [nx + B[0], ny + B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp radians within 0 and 2PI
|
||||
* @param r
|
||||
*/
|
||||
static clampRadians(r: number): number {
|
||||
return (Math.PI * 2 + r) % (Math.PI * 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp rotation to even segments.
|
||||
* @param r
|
||||
* @param segments
|
||||
*/
|
||||
static clampToRotationToSegments(r: number, segments: number): number {
|
||||
const seg = (Math.PI * 2) / segments
|
||||
return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg
|
||||
}
|
||||
|
||||
/**
|
||||
* Is angle c between angles a and b?
|
||||
* @param a
|
||||
* @param b
|
||||
* @param c
|
||||
*/
|
||||
static isAngleBetween(a: number, b: number, c: number): boolean {
|
||||
if (c === a || c === b) return true
|
||||
const PI2 = Math.PI * 2
|
||||
const AB = (b - a + PI2) % PI2
|
||||
const AC = (c - a + PI2) % PI2
|
||||
return AB <= Math.PI !== AC > AB
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians.
|
||||
* @param d
|
||||
*/
|
||||
static degreesToRadians(d: number): number {
|
||||
return (d * Math.PI) / 180
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert radians to degrees.
|
||||
* @param r
|
||||
*/
|
||||
static radiansToDegrees(r: number): number {
|
||||
return (r * 180) / Math.PI
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of an arc between two points on a circle's perimeter.
|
||||
* @param C
|
||||
* @param r
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static getArcLength(
|
||||
C: number[],
|
||||
r: number,
|
||||
A: number[],
|
||||
B: number[]
|
||||
): number {
|
||||
const sweep = Utils.getSweep(C, A, B)
|
||||
return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dash offset for an arc, based on its length.
|
||||
* @param C
|
||||
* @param r
|
||||
* @param A
|
||||
* @param B
|
||||
* @param step
|
||||
*/
|
||||
static getArcDashOffset(
|
||||
C: number[],
|
||||
r: number,
|
||||
A: number[],
|
||||
B: number[],
|
||||
step: number
|
||||
): number {
|
||||
const del0 = Utils.getSweep(C, A, B)
|
||||
const len0 = Utils.getArcLength(C, r, A, B)
|
||||
const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
|
||||
return -off0 / 2 + step
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dash offset for an ellipse, based on its length.
|
||||
* @param A
|
||||
* @param step
|
||||
*/
|
||||
static getEllipseDashOffset(A: number[], step: number): number {
|
||||
const c = 2 * Math.PI * A[2]
|
||||
return -c / 2 + -step
|
||||
}
|
||||
|
||||
static getPointsBetween(a: number[], b: number[], steps = 6): number[][] {
|
||||
return Array.from(Array(steps))
|
||||
.map((_, i) => {
|
||||
|
@ -32,44 +523,6 @@ export default class Utils {
|
|||
return [x, y]
|
||||
}
|
||||
|
||||
static getCircleTangentToPoint(
|
||||
A: number[],
|
||||
r0: number,
|
||||
P: number[],
|
||||
side: number
|
||||
): number[] {
|
||||
const B = vec.lrp(A, P, 0.5),
|
||||
r1 = vec.dist(A, B),
|
||||
delta = vec.sub(B, A),
|
||||
d = vec.len(delta)
|
||||
|
||||
if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
|
||||
n = 1 / d,
|
||||
p = vec.add(A, vec.mul(delta, a * n)),
|
||||
h = Math.sqrt(r0 * r0 - a * a),
|
||||
k = vec.mul(vec.per(delta), h * n)
|
||||
|
||||
return side === 0 ? vec.add(p, k) : vec.sub(p, k)
|
||||
}
|
||||
|
||||
static shortAngleDist(a0: number, a1: number): number {
|
||||
const max = Math.PI * 2
|
||||
const da = (a1 - a0) % max
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
static angleDelta(a0: number, a1: number): number {
|
||||
return this.shortAngleDist(a0, a1)
|
||||
}
|
||||
|
||||
static getSweep(C: number[], A: number[], B: number[]): number {
|
||||
return this.angleDelta(vec.angle(C, A), vec.angle(C, B))
|
||||
}
|
||||
|
||||
static bez1d(a: number, b: number, c: number, d: number, t: number): number {
|
||||
return (
|
||||
a * (1 - t) * (1 - t) * (1 - t) +
|
||||
|
@ -167,4 +620,120 @@ export default class Utils {
|
|||
|
||||
return bounds
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bezier curve data for a spline that fits an array of points.
|
||||
* @param pts
|
||||
* @param tension
|
||||
* @param isClosed
|
||||
* @param numOfSegments
|
||||
*/
|
||||
static getCurvePoints(
|
||||
pts: number[][],
|
||||
tension = 0.5,
|
||||
isClosed = false,
|
||||
numOfSegments = 3
|
||||
): number[][] {
|
||||
const _pts = [...pts],
|
||||
len = pts.length,
|
||||
res: number[][] = [] // results
|
||||
|
||||
let t1x: number, // tension vectors
|
||||
t2x: number,
|
||||
t1y: number,
|
||||
t2y: number,
|
||||
c1: number, // cardinal points
|
||||
c2: number,
|
||||
c3: number,
|
||||
c4: number,
|
||||
st: number,
|
||||
st2: number,
|
||||
st3: number
|
||||
|
||||
// The algorithm require a previous and next point to the actual point array.
|
||||
// Check if we will draw closed or open curve.
|
||||
// If closed, copy end points to beginning and first points to end
|
||||
// If open, duplicate first points to befinning, end points to end
|
||||
if (isClosed) {
|
||||
_pts.unshift(_pts[len - 1])
|
||||
_pts.push(_pts[0])
|
||||
} else {
|
||||
//copy 1. point and insert at beginning
|
||||
_pts.unshift(_pts[0])
|
||||
_pts.push(_pts[len - 1])
|
||||
// _pts.push(_pts[len - 1])
|
||||
}
|
||||
|
||||
// For each point, calculate a segment
|
||||
for (let i = 1; i < _pts.length - 2; i++) {
|
||||
// Calculate points along segment and add to results
|
||||
for (let t = 0; t <= numOfSegments; t++) {
|
||||
// Step
|
||||
st = t / numOfSegments
|
||||
st2 = Math.pow(st, 2)
|
||||
st3 = Math.pow(st, 3)
|
||||
|
||||
// Cardinals
|
||||
c1 = 2 * st3 - 3 * st2 + 1
|
||||
c2 = -(2 * st3) + 3 * st2
|
||||
c3 = st3 - 2 * st2 + st
|
||||
c4 = st3 - st2
|
||||
|
||||
// Tension
|
||||
t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension
|
||||
t2x = (_pts[i + 2][0] - _pts[i][0]) * tension
|
||||
t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension
|
||||
t2y = (_pts[i + 2][1] - _pts[i][1]) * tension
|
||||
|
||||
// Control points
|
||||
res.push([
|
||||
c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x,
|
||||
c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
res.push(pts[pts.length - 1])
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify a line (using Ramer-Douglas-Peucker algorithm).
|
||||
* @param points An array of points as [x, y, ...][]
|
||||
* @param tolerance The minimum line distance (also called epsilon).
|
||||
* @returns Simplified array as [x, y, ...][]
|
||||
*/
|
||||
static simplify(points: number[][], tolerance = 1): number[][] {
|
||||
const len = points.length,
|
||||
a = points[0],
|
||||
b = points[len - 1],
|
||||
[x1, y1] = a,
|
||||
[x2, y2] = b
|
||||
|
||||
if (len > 2) {
|
||||
let distance = 0
|
||||
let index = 0
|
||||
const max = Math.hypot(y2 - y1, x2 - x1)
|
||||
|
||||
for (let i = 1; i < len - 1; i++) {
|
||||
const [x0, y0] = points[i],
|
||||
d =
|
||||
Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
|
||||
|
||||
if (distance > d) continue
|
||||
|
||||
distance = d
|
||||
index = i
|
||||
}
|
||||
|
||||
if (distance > tolerance) {
|
||||
const l0 = Utils.simplify(points.slice(0, index + 1), tolerance)
|
||||
const l1 = Utils.simplify(points.slice(index + 1), tolerance)
|
||||
return l0.concat(l1.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
|
|
6
types.ts
6
types.ts
|
@ -191,12 +191,10 @@ export interface GroupShape extends BaseShape {
|
|||
size: number[]
|
||||
}
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>
|
||||
export type ShapeProps<T extends Shape> = Partial<T> & {
|
||||
style?: Partial<ShapeStyles>
|
||||
}
|
||||
|
||||
export type ShapeProps<T extends Shape> = DeepPartial<T>
|
||||
|
||||
export type MutableShape =
|
||||
| DotShape
|
||||
| EllipseShape
|
||||
|
|
|
@ -198,16 +198,6 @@ export function setToArray<T>(set: Set<T>): T[] {
|
|||
|
||||
/* -------------------- Hit Tests ------------------- */
|
||||
|
||||
/**
|
||||
* Get whether a point is inside of a bounds.
|
||||
* @param A
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
export function pointInBounds(A: number[], b: Bounds): boolean {
|
||||
return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether a point is inside of a circle.
|
||||
* @param A
|
||||
|
@ -265,6 +255,16 @@ export function pointInRect(
|
|||
|
||||
/* --------------------- Bounds --------------------- */
|
||||
|
||||
/**
|
||||
* Get whether a point is inside of a bounds.
|
||||
* @param A
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
export function pointInBounds(A: number[], b: Bounds): boolean {
|
||||
return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether two bounds collide.
|
||||
* @param a Bounds
|
||||
|
|
36
yarn.lock
36
yarn.lock
|
@ -3073,6 +3073,11 @@ commander@^2.20.0:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@^6.2.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
|
@ -4141,6 +4146,18 @@ glob-to-regexp@^0.4.1:
|
|||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
version "7.1.7"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
|
||||
|
@ -5741,7 +5758,7 @@ ms@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
mz@^2.4.0:
|
||||
mz@^2.4.0, mz@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
|
||||
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
|
||||
|
@ -7372,6 +7389,18 @@ stylis@3.5.4:
|
|||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
|
||||
integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
|
||||
|
||||
sucrase@^3.19.0:
|
||||
version "3.19.0"
|
||||
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.19.0.tgz#cc9a60f731e7497766a7b710d3362260a8f9ced5"
|
||||
integrity sha512-FeMelydANPRMiOo/lxbf7NxN8bQmMVBQmKOa69BifwVhteMJzRoJNHaVBoCYmE/kpnx6VPg9ckaLumwtuAzmEA==
|
||||
dependencies:
|
||||
commander "^4.0.0"
|
||||
glob "7.1.6"
|
||||
lines-and-columns "^1.1.6"
|
||||
mz "^2.7.0"
|
||||
pirates "^4.0.1"
|
||||
ts-interface-checker "^0.1.9"
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
|
@ -7562,6 +7591,11 @@ tr46@^2.1.0:
|
|||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
ts-interface-checker@^0.1.9:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
ts-pnp@^1.1.6:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
|
||||
|
|
Ładowanie…
Reference in New Issue