diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 1ff94f7ed..000000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["next/babel"] -} diff --git a/__tests__/__snapshots__/project.test.ts.snap b/__tests__/__snapshots__/project.test.ts.snap index 21aa5b5a1..455e6a98c 100644 --- a/__tests__/__snapshots__/project.test.ts.snap +++ b/__tests__/__snapshots__/project.test.ts.snap @@ -13270,13 +13270,9 @@ Object { "code": " const draw = new Draw({ points: [ - [0, 0], - [0, 50], - [20, 80], - [56, 56], - [52, 52], - [80, 20], - [90, 90], + ...Utils.getPointsBetween([0, 0], [20, 50]), + ...Utils.getPointsBetween([20, 50], [100, 20], 3), + ...Utils.getPointsBetween([100, 20], [100, 100], 10), [100, 100], ], }) diff --git a/__tests__/code.test.ts b/__tests__/code.test.ts new file mode 100644 index 000000000..f7e5163d0 --- /dev/null +++ b/__tests__/code.test.ts @@ -0,0 +1,69 @@ +import state from 'state' +import { generateFromCode } from 'state/code/generate' +import { getShapes } from 'utils' +import * as json from './__mocks__/document.json' + +jest.useRealTimers() + +state.reset() +state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) }) +state.send('CLEARED_PAGE') + +describe('selection', () => { + it('opens and closes the code panel', () => { + expect(state.data.settings.isCodeOpen).toBe(false) + state.send('TOGGLED_CODE_PANEL_OPEN') + expect(state.data.settings.isCodeOpen).toBe(true) + state.send('TOGGLED_CODE_PANEL_OPEN') + expect(state.data.settings.isCodeOpen).toBe(false) + }) + + it('saves changes to code', () => { + expect(getShapes(state.data).length).toBe(0) + + const code = `// hello world!` + + state.send('SAVED_CODE', { code }) + + expect(state.data.document.code[state.data.currentCodeFileId].code).toBe( + code + ) + }) + + it('generates shapes', async () => { + const code = ` + const rectangle = new Rectangle({ + name: 'Test Rectangle', + point: [100, 100], + size: [200, 200], + style: { + size: SizeStyle.Medium, + color: ColorStyle.Red, + dash: DashStyle.Dotted, + }, + }) + ` + + const { controls, shapes } = await generateFromCode(state.data, code) + state.send('GENERATED_FROM_CODE', { controls, shapes }) + expect(getShapes(state.data).length).toBe(1) + }) + + it('creates a code control', () => { + null + }) + + it('updates a code control', () => { + null + }) + + it('updates a code control', () => { + null + }) + + /* -------------------- Readonly -------------------- */ + + it('does not saves changes to code when readonly', () => { + null + }) +}) diff --git a/__tests__/create.test.ts b/__tests__/create.test.ts new file mode 100644 index 000000000..ded87a0c2 --- /dev/null +++ b/__tests__/create.test.ts @@ -0,0 +1,24 @@ +import state from 'state' +import * as json from './__mocks__/document.json' + +state.reset() +state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) }) +state.send('CLEARED_PAGE') + +describe('arrow shape', () => { + it('creates a shape', () => { + null + }) + + it('cancels shape while creating', () => { + null + }) + + it('removes shape on undo and restores it on redo', () => { + null + }) + + it('does not create shape when readonly', () => { + null + }) +}) diff --git a/__tests__/shapes/arrow.test.ts b/__tests__/shapes/arrow.test.ts new file mode 100644 index 000000000..52e381751 --- /dev/null +++ b/__tests__/shapes/arrow.test.ts @@ -0,0 +1,68 @@ +import state from 'state' +import * as json from '../__mocks__/document.json' + +state.reset() +state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) }) +state.send('CLEARED_PAGE') + +describe('arrow shape', () => { + it('creates shape', () => { + null + }) + + it('cancels shape while creating', () => { + null + }) + + it('moves shape', () => { + null + }) + + it('rotates shape', () => { + null + }) + + it('measures bounds', () => { + null + }) + + it('measures rotated bounds', () => { + null + }) + + it('transforms single', () => { + null + }) + + it('transforms in a group', () => { + null + }) + + /* -------------------- Specific -------------------- */ + + it('creates compass-aligned shape with shift key', () => { + null + }) + + it('changes start handle', () => { + null + }) + + it('changes end handle', () => { + null + }) + + it('changes bend handle', () => { + null + }) + + it('resets bend handle when double-pointed', () => { + null + }) + + /* -------------------- Readonly -------------------- */ + + it('does not create shape when readonly', () => { + null + }) +}) diff --git a/__tests__/test-utils.ts b/__tests__/test-utils.ts index 4b98bc850..910a5edd6 100644 --- a/__tests__/test-utils.ts +++ b/__tests__/test-utils.ts @@ -46,3 +46,11 @@ export function idsAreSelected( ids.every((id) => selectedIds.has(id)) ) } + +export async function asyncDelay(fn: () => T): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(fn()) + }, 100) + }) +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..bc6c51008 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['next/babel'], +} diff --git a/components/code-panel/code-panel.tsx b/components/code-panel/code-panel.tsx index 63a05950f..446581256 100644 --- a/components/code-panel/code-panel.tsx +++ b/components/code-panel/code-panel.tsx @@ -82,8 +82,11 @@ export default function CodePanel(): JSX.Element { let error = null try { - const { shapes, controls } = generateFromCode(state.data, data.code) - state.send('GENERATED_FROM_CODE', { shapes, controls }) + generateFromCode(state.data, data.code).then( + ({ shapes, controls }) => { + state.send('GENERATED_FROM_CODE', { shapes, controls }) + } + ) } catch (e) { console.error('Got an error!', e) error = { message: e.message, ...getErrorLineAndColumn(e) } diff --git a/components/code-panel/types-import.ts b/components/code-panel/types-import.ts index 9d9c525cb..69f8a38f1 100644 --- a/components/code-panel/types-import.ts +++ b/components/code-panel/types-import.ts @@ -4,66 +4,10 @@ export default { name: 'types.ts', - content: ` + content: ` + -/* -------------------------------------------------- */ -/* Client State */ -/* -------------------------------------------------- */ -interface Data { - isReadOnly: boolean - settings: { - fontSize: number - isDarkMode: boolean - isCodeOpen: boolean - isStyleOpen: boolean - nudgeDistanceSmall: number - nudgeDistanceLarge: number - isToolLocked: boolean - isPenLocked: boolean - } - currentStyle: ShapeStyles - activeTool: ShapeType | 'select' - brush?: Bounds - boundsRotation: number - pointedId?: string - hoveredId?: string - editingId?: string - currentPageId: string - currentParentId: string - currentCodeFileId: string - codeControls: Record - document: TLDocument - pageStates: Record -} - -/* -------------------------------------------------- */ -/* Document */ -/* -------------------------------------------------- */ - -interface TLDocument { - id: string - name: string - pages: Record - code: Record -} - -interface Page { - id: string - type: 'page' - childIndex: number - name: string - shapes: Record -} - -interface PageState { - id: string - selectedIds: Set - camera: { - point: number[] - zoom: number - } -} enum ShapeType { Dot = 'dot', @@ -391,24 +335,27 @@ interface BaseCodeControl { interface NumberCodeControl extends BaseCodeControl { type: ControlType.Number - min?: number - max?: number + min: number + max: number value: number step: number - format?: (value: number) => number + format: (value: number) => number } interface VectorCodeControl extends BaseCodeControl { type: ControlType.Vector + min: number + max: number + step: number value: number[] isNormalized: boolean - format?: (value: number[]) => number[] + format: (value: number[]) => number[] } interface TextCodeControl extends BaseCodeControl { type: ControlType.Text value: string - format?: (value: string) => string + format: (value: string) => string } interface SelectCodeControl @@ -594,556 +541,528 @@ interface ShapeUtility { } - class CodeShape { - private _shape: Mutable - private utils: ShapeUtility - constructor(props: T) { - this._shape = createShape(props.type, props) as Mutable - this.utils = getShapeUtils(this._shape) - codeShapes.add(this) - } - export(): Mutable { - return { ...this._shape } + class Vec { + /** + * 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) } /** - * Destroy the shape. + * Negate a vector. + * @param A */ - destroy(): void { - codeShapes.delete(this) + static neg = (A: number[]): number[] => { + return [-A[0], -A[1]] } /** - * Move the shape to a point. - * @param delta + * Add vectors. + * @param A + * @param B */ - moveTo(point: number[]): CodeShape { - return this.translateTo(point) + static add = (A: number[], B: number[]): number[] => { + return [A[0] + B[0], A[1] + B[1]] } /** - * Move the shape to a point. - * @param delta + * Add scalar to vector. + * @param A + * @param B */ - translateTo(point: number[]): CodeShape { - this.utils.translateTo(this._shape, point) - return this + static addScalar = (A: number[], n: number): number[] => { + return [A[0] + n, A[1] + n] } /** - * Move the shape by a delta. - * @param delta + * Subtract vectors. + * @param A + * @param B */ - translateBy(delta: number[]): CodeShape { - this.utils.translateTo(this._shape, delta) - return this + static sub = (A: number[], B: number[]): number[] => { + return [A[0] - B[0], A[1] - B[1]] } /** - * Rotate the shape. + * Subtract scalar from vector. + * @param A + * @param B */ - rotateTo(rotation: number): CodeShape { - this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation) - return this + static subScalar = (A: number[], n: number): number[] => { + return [A[0] - n, A[1] - n] } /** - * Rotate the shape by a delta. + * Get the vector from vectors A to B. + * @param A + * @param B */ - rotateBy(rotation: number): CodeShape { - this.utils.rotateBy(this._shape, rotation) - return this + static vec = (A: number[], B: number[]): number[] => { + // A, B as vectors get the vector from A to B + return [B[0] - A[0], B[1] - A[1]] } /** - * Get the shape's bounding box. + * Vector multiplication by scalar + * @param A + * @param n */ - getBounds(): CodeShape { - this.utils.getBounds(this.shape) - return this + static mul = (A: number[], n: number): number[] => { + return [A[0] * n, A[1] * n] + } + + static mulV = (A: number[], B: number[]): number[] => { + return [A[0] * B[0], A[1] * B[1]] } /** - * Test whether a point is inside of the shape. + * Vector division by scalar. + * @param A + * @param n */ - hitTest(point: number[]): CodeShape { - this.utils.hitTest(this.shape, point) - return this + static div = (A: number[], n: number): number[] => { + return [A[0] / n, A[1] / n] } /** - * Move the shape to the back of the painting order. + * Vector division by vector. + * @param A + * @param n */ - moveToBack(): CodeShape { - const sorted = getOrderedShapes() - - if (sorted.length <= 1) return - - const first = sorted[0].childIndex - sorted.forEach((shape) => shape.childIndex++) - this.childIndex = first - - codeShapes.clear() - sorted.forEach((shape) => codeShapes.add(shape)) - - return this + static divV = (A: number[], B: number[]): number[] => { + return [A[0] / B[0], A[1] / B[1]] } /** - * Move the shape to the top of the painting order. + * Perpendicular rotation of a vector A + * @param A */ - moveToFront(): CodeShape { - const sorted = getOrderedShapes() - - if (sorted.length <= 1) return - - const ahead = sorted.slice(sorted.indexOf(this)) - const last = ahead[ahead.length - 1].childIndex - ahead.forEach((shape) => shape.childIndex--) - this.childIndex = last - - codeShapes.clear() - sorted.forEach((shape) => codeShapes.add(shape)) - - return this + static per = (A: number[]): number[] => { + return [A[1], -A[0]] } /** - * Move the shape backward in the painting order. + * Dot product + * @param A + * @param B */ - moveBackward(): CodeShape { - const sorted = getOrderedShapes() - - if (sorted.length <= 1) return - - const next = sorted[sorted.indexOf(this) - 1] - - if (!next) return - - const index = next.childIndex - next.childIndex = this.childIndex - this.childIndex = index - - codeShapes.clear() - sorted.forEach((shape) => codeShapes.add(shape)) - - return this + static dpr = (A: number[], B: number[]): number => { + return A[0] * B[0] + A[1] * B[1] } /** - * Move the shape forward in the painting order. + * Cross product (outer product) | A X B | + * @param A + * @param B */ - moveForward(): CodeShape { - const sorted = getOrderedShapes() - - if (sorted.length <= 1) return - - const next = sorted[sorted.indexOf(this) + 1] - - if (!next) return - - const index = next.childIndex - next.childIndex = this.childIndex - this.childIndex = index - - codeShapes.clear() - sorted.forEach((shape) => codeShapes.add(shape)) - - return this + static cpr = (A: number[], B: number[]): number => { + return A[0] * B[1] - B[0] * A[1] } /** - * The shape's underlying shape. + * Length of the vector squared + * @param A */ - get shape(): T { - return this._shape + static len2 = (A: number[]): number => { + return A[0] * A[0] + A[1] * A[1] } /** - * The shape's current point. + * Length of the vector + * @param A */ - get point(): number[] { - return [...this.shape.point] - } - - set point(point: number[]) { - getShapeUtils(this.shape).translateTo(this._shape, point) + static len = (A: number[]): number => { + return Math.hypot(A[0], A[1]) } /** - * The shape's rotation. + * Project A over B + * @param A + * @param B */ - get rotation(): number { - return this.shape.rotation - } - - set rotation(rotation: number) { - getShapeUtils(this.shape).rotateTo( - this._shape, - rotation, - rotation - this.shape.rotation - ) + static pry = (A: number[], B: number[]): number => { + return Vec.dpr(A, B) / Vec.len(B) } /** - * The shape's color style. + * Get normalized / unit vector. + * @param A */ - get color(): ColorStyle { - return this.shape.style.color - } - - set color(color: ColorStyle) { - getShapeUtils(this.shape).applyStyles(this._shape, { color }) + static uni = (A: number[]): number[] => { + return Vec.div(A, Vec.len(A)) } /** - * The shape's dash style. + * Get normalized / unit vector. + * @param A */ - get dash(): DashStyle { - return this.shape.style.dash - } - - set dash(dash: DashStyle) { - getShapeUtils(this.shape).applyStyles(this._shape, { dash }) + static normalize = (A: number[]): number[] => { + return Vec.uni(A) } /** - * The shape's stroke width. + * Get the tangent between two vectors. + * @param A + * @param B + * @returns */ - get strokeWidth(): SizeStyle { - return this.shape.style.size - } - - set strokeWidth(size: SizeStyle) { - getShapeUtils(this.shape).applyStyles(this._shape, { size }) + static tangent = (A: number[], B: number[]): number[] => { + return Vec.normalize(Vec.sub(A, B)) } /** - * The shape's index in the painting order. + * Dist length from A to B squared. + * @param A + * @param B */ - get childIndex(): number { - return this.shape.childIndex + static dist2 = (A: number[], B: number[]): number => { + return Vec.len2(Vec.sub(A, B)) } - set childIndex(childIndex: number) { - getShapeUtils(this.shape).setProperty(this._shape, 'childIndex', childIndex) + /** + * Dist length from A to B + * @param A + * @param B + */ + static dist = (A: number[], B: number[]): number => { + return Math.hypot(A[1] - B[1], A[0] - B[0]) + } + + /** + * A faster, though less accurate method for testing distances. Maybe faster? + * @param A + * @param B + * @returns + */ + static fastDist = (A: number[], B: number[]): number[] => { + const V = [B[0] - A[0], B[1] - A[1]] + const aV = [Math.abs(V[0]), Math.abs(V[1])] + let r = 1 / Math.max(aV[0], aV[1]) + r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289) + return [V[0] * r, V[1] * r] + } + + /** + * Angle between vector A and vector B in radians + * @param A + * @param B + */ + static ang = (A: number[], B: number[]): number => { + return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B)) + } + + /** + * Angle between vector A and vector B in radians + * @param A + * @param B + */ + static angle = (A: number[], B: number[]): number => { + return Math.atan2(B[1] - A[1], B[0] - A[0]) + } + + /** + * Mean between two vectors or mid vector between two vectors + * @param A + * @param B + */ + static med = (A: number[], B: number[]): number[] => { + return Vec.mul(Vec.add(A, B), 0.5) + } + + /** + * Vector rotation by r (radians) + * @param A + * @param r rotation in radians + */ + static rot = (A: number[], r: number): number[] => { + return [ + A[0] * Math.cos(r) - A[1] * Math.sin(r), + A[0] * Math.sin(r) + A[1] * Math.cos(r), + ] + } + + /** + * Rotate a vector around another vector by r (radians) + * @param A vector + * @param C center + * @param r rotation in radians + */ + static rotWith = (A: number[], C: number[], r: number): number[] => { + if (r === 0) return A + + const s = Math.sin(r) + const c = Math.cos(r) + + const px = A[0] - C[0] + const py = A[1] - C[1] + + const nx = px * c - py * s + const ny = px * s + py * c + + return [nx + C[0], ny + C[1]] + } + + /** + * Check of two vectors are identical. + * @param A + * @param B + */ + static isEqual = (A: number[], B: number[]): boolean => { + return A[0] === B[0] && A[1] === B[1] + } + + /** + * Interpolate vector A to B with a scalar t + * @param A + * @param B + * @param t scalar + */ + static lrp = (A: number[], B: number[], t: number): number[] => { + return Vec.add(A, Vec.mul(Vec.vec(A, B), t)) + } + + /** + * Interpolate from A to B when curVAL goes fromVAL: number[] => to + * @param A + * @param B + * @param from Starting value + * @param to Ending value + * @param s Strength + */ + static int = ( + A: number[], + B: number[], + from: number, + to: number, + s = 1 + ): number[] => { + const t = (Vec.clamp(from, to) - from) / (to - from) + return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s)) + } + + /** + * Get the angle between the three vectors A, B, and C. + * @param p1 + * @param pc + * @param p2 + */ + static ang3 = (p1: number[], pc: number[], p2: number[]): number => { + // this, + const v1 = Vec.vec(pc, p1) + const v2 = Vec.vec(pc, p2) + return Vec.ang(v1, v2) + } + + /** + * Absolute value of a vector. + * @param A + * @returns + */ + static abs = (A: number[]): number[] => { + return [Math.abs(A[0]), Math.abs(A[1])] + } + + static rescale = (a: number[], n: number): number[] => { + const l = Vec.len(a) + return [(n * a[0]) / l, (n * a[1]) / l] + } + + /** + * Get whether p1 is left of p2, relative to pc. + * @param p1 + * @param pc + * @param p2 + */ + static isLeft = (p1: number[], pc: number[], p2: number[]): number => { + // isLeft: >0 for counterclockwise + // =0 for none (degenerate) + // <0 for clockwise + return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1]) + } + + static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => { + return Vec.isLeft(p1, pc, p2) > 0 + } + + static round = (a: number[], d = 5): number[] => { + return a.map((v) => Number(v.toPrecision(d))) + } + + /** + * Get the minimum distance from a point P to a line with a segment AB. + * @param A The start of the line. + * @param B The end of the line. + * @param P A point. + * @returns + */ + // static distanceToLine(A: number[], B: number[], P: number[]) { + // const delta = sub(B, A) + // const angle = Math.atan2(delta[1], delta[0]) + // const dir = rot(sub(P, A), -angle) + // return dir[1] + // } + + /** + * Get the nearest point on a line segment AB. + * @param A The start of the line. + * @param B The end of the line. + * @param P A point. + * @param clamp Whether to clamp the resulting point to the segment. + * @returns + */ + // static nearestPointOnLine( + // A: number[], + // B: number[], + // P: number[], + // clamp = true + // ) { + // const delta = sub(B, A) + // const length = len(delta) + // const angle = Math.atan2(delta[1], delta[0]) + // const dir = rot(sub(P, A), -angle) + + // if (clamp) { + // if (dir[0] < 0) return A + // if (dir[0] > length) return B + // } + + // return add(A, div(mul(delta, dir[0]), length)) + // } + + /** + * Get the nearest point on a line with a known unit vector that passes through point A + * @param A Any point on the line + * @param u The unit vector for the line. + * @param P A point not on the line to test. + * @returns + */ + static nearestPointOnLineThroughPoint = ( + A: number[], + u: number[], + P: number[] + ): number[] => { + return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u))) + } + + /** + * Distance between a point and a line with a known unit vector that passes through a point. + * @param A Any point on the line + * @param u The unit vector for the line. + * @param P A point not on the line to test. + * @returns + */ + static distanceToLineThroughPoint = ( + A: number[], + u: number[], + P: number[] + ): number => { + return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P)) + } + + /** + * Get the nearest point on a line segment between A and B + * @param A The start of the line segment + * @param B The end of the line segment + * @param P The off-line point + * @param clamp Whether to clamp the point between A and B. + * @returns + */ + static nearestPointOnLineSegment = ( + A: number[], + B: number[], + P: number[], + clamp = true + ): number[] => { + const delta = Vec.sub(B, A) + const length = Vec.len(delta) + const u = Vec.div(delta, length) + + const pt = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u))) + + if (clamp) { + const da = Vec.dist(A, pt) + const db = Vec.dist(B, pt) + + if (db < da && da > length) return B + if (da < db && db > length) return A + } + + return pt + } + + /** + * Distance between a point and the nearest point on a line segment between A and B + * @param A The start of the line segment + * @param B The end of the line segment + * @param P The off-line point + * @param clamp Whether to clamp the point between A and B. + * @returns + */ + static distanceToLineSegment = ( + A: number[], + B: number[], + P: number[], + clamp = true + ): number => { + return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp)) + } + + /** + * Push a point A towards point B by a given distance. + * @param A + * @param B + * @param d + * @returns + */ + static nudge = (A: number[], B: number[], d: number): number[] => { + return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d)) + } + + /** + * Push a point in a given angle by a given distance. + * @param A + * @param B + * @param d + */ + static nudgeAtAngle = (A: number[], a: number, d: number): number[] => { + return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]] + } + + /** + * Round a vector to a precision length. + * @param a + * @param n + */ + static toPrecision = (a: number[], n = 4): number[] => { + return [+a[0].toPrecision(n), +a[1].toPrecision(n)] + } + + /** + * Get a number of points between two points. + * @param a + * @param b + * @param steps + */ + static pointsBetween = (a: number[], b: number[], steps = 6): number[][] => { + return Array.from(Array(steps)) + .map((_, i) => { + const t = i / steps + return t * t * t + }) + .map((t) => [...Vec.lrp(a, b, t), (1 - t) / 2]) } } -/** - * ## Dot - */ - class Dot extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - parentId: (window as any).currentPageId, - type: ShapeType.Dot, - isGenerated: true, - name: 'Dot', - childIndex: 0, - point: [0, 0], - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { - ...defaultStyle, - ...props.style, - isFilled: true, - }, - }) - } -} -/** - * ## Ellipse - */ - class Ellipse extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - parentId: (window as any).currentPageId, - type: ShapeType.Ellipse, - isGenerated: true, - name: 'Ellipse', - childIndex: 0, - point: [0, 0], - radiusX: 50, - radiusY: 50, - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { ...defaultStyle, ...props.style }, - }) - } - get radiusX(): number { - return this.shape.radiusX - } - get radiusY(): number { - return this.shape.radiusY - } -} - -/** - * ## Line - */ - class Line extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - parentId: (window as any).currentPageId, - type: ShapeType.Line, - isGenerated: true, - name: 'Line', - childIndex: 0, - point: [0, 0], - direction: [-0.5, 0.5], - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { - ...defaultStyle, - ...props.style, - isFilled: false, - }, - }) - } - - get direction(): number[] { - return this.shape.direction - } -} - -/** - * ## Polyline - */ - class Polyline extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - parentId: (window as any).currentPageId, - type: ShapeType.Polyline, - isGenerated: true, - name: 'Polyline', - childIndex: 0, - point: [0, 0], - points: [[0, 0]], - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - style: defaultStyle, - ...props, - }) - } - - get points(): number[][] { - return this.shape.points - } -} - -/** - * ## Ray - */ - class Ray extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - type: ShapeType.Ray, - isGenerated: true, - name: 'Ray', - parentId: 'page1', - childIndex: 0, - point: [0, 0], - direction: [0, 1], - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { - ...defaultStyle, - ...props.style, - isFilled: false, - }, - }) - } - - get direction(): number[] { - return this.shape.direction - } -} - -/** - * ## Rectangle - */ - class Rectangle extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - parentId: (window as any).currentPageId, - type: ShapeType.Rectangle, - isGenerated: true, - name: 'Rectangle', - childIndex: 0, - point: [0, 0], - size: [100, 100], - rotation: 0, - radius: 2, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { ...defaultStyle, ...props.style }, - }) - } - - get size(): number[] { - return this.shape.size - } -} - -/** - * ## Draw - */ - class Arrow extends CodeShape { - constructor( - props = {} as ShapeProps & { start: number[]; end: number[] } - ) { - const { start = [0, 0], end = [0, 0] } = props - - const { - point = [0, 0], - handles = { - start: { - id: 'start', - index: 0, - point: start, - }, - end: { - id: 'end', - index: 1, - point: end, - }, - bend: { - id: 'bend', - index: 2, - point: Vec.med(start, end), - }, - }, - } = props - - super({ - id: uniqueId(), - seed: Math.random(), - type: ShapeType.Arrow, - isGenerated: false, - name: 'Arrow', - parentId: 'page1', - childIndex: 0, - point, - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - bend: 0, - handles, - decorations: { - start: null, - middle: null, - end: Decoration.Arrow, - }, - ...props, - style: { - ...defaultStyle, - ...props.style, - isFilled: false, - }, - }) - } - - get start(): number[] { - return this.shape.handles.start.point - } - - set start(point: number[]) { - getShapeUtils(this.shape).onHandleChange(this.shape, { - start: { ...this.shape.handles.start, point }, - }) - } - - get middle(): number[] { - return this.shape.handles.bend.point - } - - set middle(point: number[]) { - getShapeUtils(this.shape).onHandleChange(this.shape, { - bend: { ...this.shape.handles.bend, point }, - }) - } - - get end(): number[] { - return this.shape.handles.end.point - } - - set end(point: number[]) { - getShapeUtils(this.shape).onHandleChange(this.shape, { - end: { ...this.shape.handles.end, point }, - }) - } - - get bend(): number { - return this.shape.bend - } -} - -/** - * ## Draw - */ - class Draw extends CodeShape { - constructor(props = {} as ShapeProps) { - super({ - id: uniqueId(), - seed: Math.random(), - type: ShapeType.Draw, - isGenerated: false, - name: 'Draw', - parentId: 'page1', - childIndex: 0, - point: [0, 0], - points: [], - rotation: 0, - isAspectRatioLocked: false, - isLocked: false, - isHidden: false, - ...props, - style: { - ...defaultStyle, - ...props.style, - }, - }) - } -} - -/** - * ## Utils - */ class Utils { /** * Linear interpolation betwen two numbers. @@ -1890,524 +1809,623 @@ interface ShapeUtility { } } -// A big collection of vector utilities. Collected into a class to improve logging / packaging. - class Vec { - /** - * 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) + + + class CodeShape { + private _shape: Mutable + private utils: ShapeUtility + + constructor(props: T) { + this._shape = createShape(props.type, props) as Mutable + this.utils = getShapeUtils(this._shape) + codeShapes.add(this) + } + + export(): Mutable { + return { ...this._shape } } /** - * Negate a vector. - * @param A + * Destroy the shape. */ - static neg = (A: number[]): number[] => { - return [-A[0], -A[1]] + destroy(): void { + codeShapes.delete(this) } /** - * Add vectors. - * @param A - * @param B + * Move the shape to a point. + * @param delta */ - static add = (A: number[], B: number[]): number[] => { - return [A[0] + B[0], A[1] + B[1]] + moveTo(point: number[]): CodeShape { + return this.translateTo(point) } /** - * Add scalar to vector. - * @param A - * @param B + * Move the shape to a point. + * @param delta */ - static addScalar = (A: number[], n: number): number[] => { - return [A[0] + n, A[1] + n] + translateTo(point: number[]): CodeShape { + this.utils.translateTo(this._shape, point) + return this } /** - * Subtract vectors. - * @param A - * @param B + * Move the shape by a delta. + * @param delta */ - static sub = (A: number[], B: number[]): number[] => { - return [A[0] - B[0], A[1] - B[1]] + translateBy(delta: number[]): CodeShape { + this.utils.translateTo(this._shape, delta) + return this } /** - * Subtract scalar from vector. - * @param A - * @param B + * Rotate the shape. */ - static subScalar = (A: number[], n: number): number[] => { - return [A[0] - n, A[1] - n] + rotateTo(rotation: number): CodeShape { + this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation) + return this } /** - * Get the vector from vectors A to B. - * @param A - * @param B + * Rotate the shape by a delta. */ - static vec = (A: number[], B: number[]): number[] => { - // A, B as vectors get the vector from A to B - return [B[0] - A[0], B[1] - A[1]] + rotateBy(rotation: number): CodeShape { + this.utils.rotateBy(this._shape, rotation) + return this } /** - * Vector multiplication by scalar - * @param A - * @param n + * Get the shape's bounding box. */ - static mul = (A: number[], n: number): number[] => { - return [A[0] * n, A[1] * n] - } - - static mulV = (A: number[], B: number[]): number[] => { - return [A[0] * B[0], A[1] * B[1]] + getBounds(): CodeShape { + this.utils.getBounds(this.shape) + return this } /** - * Vector division by scalar. - * @param A - * @param n + * Test whether a point is inside of the shape. */ - static div = (A: number[], n: number): number[] => { - return [A[0] / n, A[1] / n] + hitTest(point: number[]): CodeShape { + this.utils.hitTest(this.shape, point) + return this } /** - * Vector division by vector. - * @param A - * @param n + * Move the shape to the back of the painting order. */ - static divV = (A: number[], B: number[]): number[] => { - return [A[0] / B[0], A[1] / B[1]] + moveToBack(): CodeShape { + const sorted = getOrderedShapes() + + if (sorted.length <= 1) return + + const first = sorted[0].childIndex + sorted.forEach((shape) => shape.childIndex++) + this.childIndex = first + + codeShapes.clear() + sorted.forEach((shape) => codeShapes.add(shape)) + + return this } /** - * Perpendicular rotation of a vector A - * @param A + * Move the shape to the top of the painting order. */ - static per = (A: number[]): number[] => { - return [A[1], -A[0]] + moveToFront(): CodeShape { + const sorted = getOrderedShapes() + + if (sorted.length <= 1) return + + const ahead = sorted.slice(sorted.indexOf(this)) + const last = ahead[ahead.length - 1].childIndex + ahead.forEach((shape) => shape.childIndex--) + this.childIndex = last + + codeShapes.clear() + sorted.forEach((shape) => codeShapes.add(shape)) + + return this } /** - * Dot product - * @param A - * @param B + * Move the shape backward in the painting order. */ - static dpr = (A: number[], B: number[]): number => { - return A[0] * B[0] + A[1] * B[1] + moveBackward(): CodeShape { + const sorted = getOrderedShapes() + + if (sorted.length <= 1) return + + const next = sorted[sorted.indexOf(this) - 1] + + if (!next) return + + const index = next.childIndex + next.childIndex = this.childIndex + this.childIndex = index + + codeShapes.clear() + sorted.forEach((shape) => codeShapes.add(shape)) + + return this } /** - * Cross product (outer product) | A X B | - * @param A - * @param B + * Move the shape forward in the painting order. */ - static cpr = (A: number[], B: number[]): number => { - return A[0] * B[1] - B[0] * A[1] + moveForward(): CodeShape { + const sorted = getOrderedShapes() + + if (sorted.length <= 1) return + + const next = sorted[sorted.indexOf(this) + 1] + + if (!next) return + + const index = next.childIndex + next.childIndex = this.childIndex + this.childIndex = index + + codeShapes.clear() + sorted.forEach((shape) => codeShapes.add(shape)) + + return this } /** - * Length of the vector squared - * @param A + * The shape's underlying shape. */ - static len2 = (A: number[]): number => { - return A[0] * A[0] + A[1] * A[1] + get shape(): T { + return this._shape } /** - * Length of the vector - * @param A + * The shape's current point. */ - static len = (A: number[]): number => { - return Math.hypot(A[0], A[1]) + get point(): number[] { + return [...this.shape.point] + } + + set point(point: number[]) { + getShapeUtils(this.shape).translateTo(this._shape, point) } /** - * Project A over B - * @param A - * @param B + * The shape's rotation. */ - static pry = (A: number[], B: number[]): number => { - return Vec.dpr(A, B) / Vec.len(B) + get rotation(): number { + return this.shape.rotation + } + + set rotation(rotation: number) { + getShapeUtils(this.shape).rotateTo( + this._shape, + rotation, + rotation - this.shape.rotation + ) } /** - * Get normalized / unit vector. - * @param A + * The shape's color style. */ - static uni = (A: number[]): number[] => { - return Vec.div(A, Vec.len(A)) + get color(): ColorStyle { + return this.shape.style.color + } + + set color(color: ColorStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { color }) } /** - * Get normalized / unit vector. - * @param A + * The shape's dash style. */ - static normalize = (A: number[]): number[] => { - return Vec.uni(A) + get dash(): DashStyle { + return this.shape.style.dash + } + + set dash(dash: DashStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { dash }) } /** - * Get the tangent between two vectors. - * @param A - * @param B - * @returns + * The shape's stroke width. */ - static tangent = (A: number[], B: number[]): number[] => { - return Vec.normalize(Vec.sub(A, B)) + get strokeWidth(): SizeStyle { + return this.shape.style.size + } + + set strokeWidth(size: SizeStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { size }) } /** - * Dist length from A to B squared. - * @param A - * @param B + * The shape's index in the painting order. */ - static dist2 = (A: number[], B: number[]): number => { - return Vec.len2(Vec.sub(A, B)) + get childIndex(): number { + return this.shape.childIndex } - /** - * Dist length from A to B - * @param A - * @param B - */ - static dist = (A: number[], B: number[]): number => { - return Math.hypot(A[1] - B[1], A[0] - B[0]) - } - - /** - * A faster, though less accurate method for testing distances. Maybe faster? - * @param A - * @param B - * @returns - */ - static fastDist = (A: number[], B: number[]): number[] => { - const V = [B[0] - A[0], B[1] - A[1]] - const aV = [Math.abs(V[0]), Math.abs(V[1])] - let r = 1 / Math.max(aV[0], aV[1]) - r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289) - return [V[0] * r, V[1] * r] - } - - /** - * Angle between vector A and vector B in radians - * @param A - * @param B - */ - static ang = (A: number[], B: number[]): number => { - return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B)) - } - - /** - * Angle between vector A and vector B in radians - * @param A - * @param B - */ - static angle = (A: number[], B: number[]): number => { - return Math.atan2(B[1] - A[1], B[0] - A[0]) - } - - /** - * Mean between two vectors or mid vector between two vectors - * @param A - * @param B - */ - static med = (A: number[], B: number[]): number[] => { - return Vec.mul(Vec.add(A, B), 0.5) - } - - /** - * Vector rotation by r (radians) - * @param A - * @param r rotation in radians - */ - static rot = (A: number[], r: number): number[] => { - return [ - A[0] * Math.cos(r) - A[1] * Math.sin(r), - A[0] * Math.sin(r) + A[1] * Math.cos(r), - ] - } - - /** - * Rotate a vector around another vector by r (radians) - * @param A vector - * @param C center - * @param r rotation in radians - */ - static rotWith = (A: number[], C: number[], r: number): number[] => { - if (r === 0) return A - - const s = Math.sin(r) - const c = Math.cos(r) - - const px = A[0] - C[0] - const py = A[1] - C[1] - - const nx = px * c - py * s - const ny = px * s + py * c - - return [nx + C[0], ny + C[1]] - } - - /** - * Check of two vectors are identical. - * @param A - * @param B - */ - static isEqual = (A: number[], B: number[]): boolean => { - return A[0] === B[0] && A[1] === B[1] - } - - /** - * Interpolate vector A to B with a scalar t - * @param A - * @param B - * @param t scalar - */ - static lrp = (A: number[], B: number[], t: number): number[] => { - return Vec.add(A, Vec.mul(Vec.vec(A, B), t)) - } - - /** - * Interpolate from A to B when curVAL goes fromVAL: number[] => to - * @param A - * @param B - * @param from Starting value - * @param to Ending value - * @param s Strength - */ - static int = ( - A: number[], - B: number[], - from: number, - to: number, - s = 1 - ): number[] => { - const t = (Vec.clamp(from, to) - from) / (to - from) - return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s)) - } - - /** - * Get the angle between the three vectors A, B, and C. - * @param p1 - * @param pc - * @param p2 - */ - static ang3 = (p1: number[], pc: number[], p2: number[]): number => { - // this, - const v1 = Vec.vec(pc, p1) - const v2 = Vec.vec(pc, p2) - return Vec.ang(v1, v2) - } - - /** - * Absolute value of a vector. - * @param A - * @returns - */ - static abs = (A: number[]): number[] => { - return [Math.abs(A[0]), Math.abs(A[1])] - } - - static rescale = (a: number[], n: number): number[] => { - const l = Vec.len(a) - return [(n * a[0]) / l, (n * a[1]) / l] - } - - /** - * Get whether p1 is left of p2, relative to pc. - * @param p1 - * @param pc - * @param p2 - */ - static isLeft = (p1: number[], pc: number[], p2: number[]): number => { - // isLeft: >0 for counterclockwise - // =0 for none (degenerate) - // <0 for clockwise - return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1]) - } - - static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => { - return Vec.isLeft(p1, pc, p2) > 0 - } - - static round = (a: number[], d = 5): number[] => { - return a.map((v) => Number(v.toPrecision(d))) - } - - /** - * Get the minimum distance from a point P to a line with a segment AB. - * @param A The start of the line. - * @param B The end of the line. - * @param P A point. - * @returns - */ - // static distanceToLine(A: number[], B: number[], P: number[]) { - // const delta = sub(B, A) - // const angle = Math.atan2(delta[1], delta[0]) - // const dir = rot(sub(P, A), -angle) - // return dir[1] - // } - - /** - * Get the nearest point on a line segment AB. - * @param A The start of the line. - * @param B The end of the line. - * @param P A point. - * @param clamp Whether to clamp the resulting point to the segment. - * @returns - */ - // static nearestPointOnLine( - // A: number[], - // B: number[], - // P: number[], - // clamp = true - // ) { - // const delta = sub(B, A) - // const length = len(delta) - // const angle = Math.atan2(delta[1], delta[0]) - // const dir = rot(sub(P, A), -angle) - - // if (clamp) { - // if (dir[0] < 0) return A - // if (dir[0] > length) return B - // } - - // return add(A, div(mul(delta, dir[0]), length)) - // } - - /** - * Get the nearest point on a line with a known unit vector that passes through point A - * @param A Any point on the line - * @param u The unit vector for the line. - * @param P A point not on the line to test. - * @returns - */ - static nearestPointOnLineThroughPoint = ( - A: number[], - u: number[], - P: number[] - ): number[] => { - return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u))) - } - - /** - * Distance between a point and a line with a known unit vector that passes through a point. - * @param A Any point on the line - * @param u The unit vector for the line. - * @param P A point not on the line to test. - * @returns - */ - static distanceToLineThroughPoint = ( - A: number[], - u: number[], - P: number[] - ): number => { - return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P)) - } - - /** - * Get the nearest point on a line segment between A and B - * @param A The start of the line segment - * @param B The end of the line segment - * @param P The off-line point - * @param clamp Whether to clamp the point between A and B. - * @returns - */ - static nearestPointOnLineSegment = ( - A: number[], - B: number[], - P: number[], - clamp = true - ): number[] => { - const delta = Vec.sub(B, A) - const length = Vec.len(delta) - const u = Vec.div(delta, length) - - const pt = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u))) - - if (clamp) { - const da = Vec.dist(A, pt) - const db = Vec.dist(B, pt) - - if (db < da && da > length) return B - if (da < db && db > length) return A - } - - return pt - } - - /** - * Distance between a point and the nearest point on a line segment between A and B - * @param A The start of the line segment - * @param B The end of the line segment - * @param P The off-line point - * @param clamp Whether to clamp the point between A and B. - * @returns - */ - static distanceToLineSegment = ( - A: number[], - B: number[], - P: number[], - clamp = true - ): number => { - return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp)) - } - - /** - * Push a point A towards point B by a given distance. - * @param A - * @param B - * @param d - * @returns - */ - static nudge = (A: number[], B: number[], d: number): number[] => { - return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d)) - } - - /** - * Push a point in a given angle by a given distance. - * @param A - * @param B - * @param d - */ - static nudgeAtAngle = (A: number[], a: number, d: number): number[] => { - return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]] - } - - /** - * Round a vector to a precision length. - * @param a - * @param n - */ - static toPrecision = (a: number[], n = 4): number[] => { - return [+a[0].toPrecision(n), +a[1].toPrecision(n)] - } - - /** - * Get a number of points between two points. - * @param a - * @param b - * @param steps - */ - static pointsBetween = (a: number[], b: number[], steps = 6): number[][] => { - return Array.from(Array(steps)) - .map((_, i) => { - const t = i / steps - return t * t * t - }) - .map((t) => [...Vec.lrp(a, b, t), (1 - t) / 2]) + set childIndex(childIndex: number) { + getShapeUtils(this.shape).setProperty(this._shape, 'childIndex', childIndex) } } + + + + class Dot extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + parentId: (window as any).currentPageId, + type: ShapeType.Dot, + isGenerated: true, + name: 'Dot', + childIndex: 0, + point: [0, 0], + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + isFilled: true, + }, + }) + } +} + + + + + class Ellipse extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + parentId: (window as any).currentPageId, + type: ShapeType.Ellipse, + isGenerated: true, + name: 'Ellipse', + childIndex: 0, + point: [0, 0], + radiusX: 50, + radiusY: 50, + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { ...defaultStyle, ...props.style }, + }) + } + + get radiusX(): number { + return this.shape.radiusX + } + + get radiusY(): number { + return this.shape.radiusY + } +} + + + + + class Line extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + parentId: (window as any).currentPageId, + type: ShapeType.Line, + isGenerated: true, + name: 'Line', + childIndex: 0, + point: [0, 0], + direction: [-0.5, 0.5], + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + isFilled: false, + }, + }) + } + + get direction(): number[] { + return this.shape.direction + } +} + + + + + class Polyline extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + parentId: (window as any).currentPageId, + type: ShapeType.Polyline, + isGenerated: true, + name: 'Polyline', + childIndex: 0, + point: [0, 0], + points: [[0, 0]], + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + }, + }) + } + + get points(): number[][] { + return this.shape.points + } +} + + + + + class Ray extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + type: ShapeType.Ray, + isGenerated: true, + name: 'Ray', + parentId: 'page1', + childIndex: 0, + point: [0, 0], + direction: [0, 1], + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + isFilled: false, + }, + }) + } + + get direction(): number[] { + return this.shape.direction + } +} + + + + + class Arrow extends CodeShape { + constructor( + props = {} as ShapeProps & { start: number[]; end: number[] } + ) { + const { start = [0, 0], end = [0, 0] } = props + + const { + point = [0, 0], + handles = { + start: { + id: 'start', + index: 0, + point: start, + }, + end: { + id: 'end', + index: 1, + point: end, + }, + bend: { + id: 'bend', + index: 2, + point: Vec.med(start, end), + }, + }, + } = props + + super({ + id: uniqueId(), + seed: Math.random(), + type: ShapeType.Arrow, + isGenerated: false, + name: 'Arrow', + parentId: 'page1', + childIndex: 0, + point, + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + bend: 0, + handles, + decorations: { + start: null, + middle: null, + end: Decoration.Arrow, + }, + ...props, + style: { + ...defaultStyle, + ...props.style, + isFilled: false, + }, + }) + } + + get start(): number[] { + return this.shape.handles.start.point + } + + set start(point: number[]) { + getShapeUtils(this.shape).onHandleChange(this.shape, { + start: { ...this.shape.handles.start, point }, + }) + } + + get middle(): number[] { + return this.shape.handles.bend.point + } + + set middle(point: number[]) { + getShapeUtils(this.shape).onHandleChange(this.shape, { + bend: { ...this.shape.handles.bend, point }, + }) + } + + get end(): number[] { + return this.shape.handles.end.point + } + + set end(point: number[]) { + getShapeUtils(this.shape).onHandleChange(this.shape, { + end: { ...this.shape.handles.end, point }, + }) + } + + get bend(): number { + return this.shape.bend + } +} + + + + + class Draw extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + type: ShapeType.Draw, + isGenerated: false, + parentId: (window as any).currentPageId, + name: 'Draw', + childIndex: 0, + point: [0, 0], + points: [], + rotation: 0, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + }, + }) + } +} + + + + + class Rectangle extends CodeShape { + constructor(props = {} as ShapeProps) { + super({ + id: uniqueId(), + seed: Math.random(), + parentId: (window as any).currentPageId, + type: ShapeType.Rectangle, + isGenerated: true, + name: 'Rectangle', + childIndex: 0, + point: [0, 0], + size: [100, 100], + rotation: 0, + radius: 2, + isAspectRatioLocked: false, + isLocked: false, + isHidden: false, + ...props, + style: { + ...defaultStyle, + ...props.style, + }, + }) + } + + get size(): number[] { + return this.shape.size + } +} + + + + +class Control { + control: T + + constructor(control: Omit) { + this.control = { ...control, id: uniqueId() } as T + codeControls.add(this.control) + + // Could there be a better way to prevent this? + // When updating, constructor should just bind to + // the existing control rather than creating a new one? + if (!(window as any).isUpdatingCode) { + controls[this.control.label] = this.control.value + } + } + + destroy(): void { + codeControls.delete(this.control) + delete controls[this.control.label] + } + + get value(): T['value'] { + return this.control.value + } + + set value(value: T['value']) { + this.control.value = value + } +} + +type ControlProps = Omit, 'id' | 'type'> + +class NumberControl extends Control { + constructor(options: ControlProps) { + const { label = 'Number', value = 0, step = 1 } = options + super({ + type: ControlType.Number, + ...options, + label, + value, + step, + }) + } +} + +class VectorControl extends Control { + constructor(options: ControlProps) { + const { label = 'Vector', value = [0, 0], isNormalized = false } = options + super({ + type: ControlType.Vector, + ...options, + label, + value, + isNormalized, + }) + } +} + + +declare const controls: {[key:string]: any} = {} `, } diff --git a/components/controls-panel/control.tsx b/components/controls-panel/control.tsx index 3f19ea478..3abaf10ea 100644 --- a/components/controls-panel/control.tsx +++ b/components/controls-panel/control.tsx @@ -50,14 +50,21 @@ function NumberControl({ id, min, max, step, value }: NumberCodeControl) { ) } -function VectorControl({ id, value, isNormalized }: VectorCodeControl) { +function VectorControl({ + id, + value, + min = -Infinity, + max = Infinity, + step = 0.01, + isNormalized = false, +}: VectorCodeControl) { return ( state.send('CHANGED_CODE_CONTROL', { @@ -67,9 +74,9 @@ function VectorControl({ id, value, isNormalized }: VectorCodeControl) { /> state.send('CHANGED_CODE_CONTROL', { @@ -79,9 +86,9 @@ function VectorControl({ id, value, isNormalized }: VectorCodeControl) { /> state.send('CHANGED_CODE_CONTROL', { @@ -91,9 +98,9 @@ function VectorControl({ id, value, isNormalized }: VectorCodeControl) { /> state.send('CHANGED_CODE_CONTROL', { diff --git a/components/controls-panel/controls-panel.tsx b/components/controls-panel/controls-panel.tsx index 70bf0d17f..87daabf12 100644 --- a/components/controls-panel/controls-panel.tsx +++ b/components/controls-panel/controls-panel.tsx @@ -17,7 +17,12 @@ export default function ControlPanel(): JSX.Element { const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0) return ( - + {isOpen ? ( diff --git a/components/editor.tsx b/components/editor.tsx index 2d0188eaf..b1b4a37ce 100644 --- a/components/editor.tsx +++ b/components/editor.tsx @@ -7,6 +7,7 @@ import StylePanel from './style-panel/style-panel' import styled from 'styles' import PagePanel from './page-panel/page-panel' import CodePanel from './code-panel/code-panel' +import ControlsPanel from './controls-panel/controls-panel' export default function Editor(): JSX.Element { useKeyboardEvents() @@ -16,6 +17,7 @@ export default function Editor(): JSX.Element { + diff --git a/components/panel.tsx b/components/panel.tsx index 4bb80a643..a1e9955e1 100644 --- a/components/panel.tsx +++ b/components/panel.tsx @@ -18,6 +18,10 @@ export const Root = styled('div', { }, variant: { code: {}, + controls: { + position: 'absolute', + right: 156, + }, }, isOpen: { true: {}, diff --git a/jest.config.js b/jest.config.js index 78bbd561b..7bec5006d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,7 @@ module.exports = { - roots: [''], testEnvironment: 'jsdom', - moduleFileExtensions: ['ts', 'tsx', 'mjs', 'js', 'json', 'jsx'], - testPathIgnorePatterns: ['[/\\\\](node_modules|.next)[/\\\\]'], - transformIgnorePatterns: ['node_modules/(?!(browser-fs-access)/)'], + testPathIgnorePatterns: ['node_modules', '.next'], + transformIgnorePatterns: ['node_modules/(?!(sucrase|browser-fs-access)/)'], transform: { '^.+\\.(ts|tsx|mjs)$': 'babel-jest', }, diff --git a/package.json b/package.json index b61b2ef80..4109bd439 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "dev": "next dev", "format": "prettier --write .", "lint": "eslint . --ext ts --ext tsx --ext js", - "scripts": "node scripts/type-gen && yarn format", + "scripts": "node scripts/type-gen && prettier --write './components/code-panel/types-import.ts'", "start": "next start", "test-all": "yarn lint && yarn type-check && yarn test", "test:update": "jest --updateSnapshot", "test:watch": "jest --watchAll", - "test": "jest", + "test": "jest --watchAll=false", "type-check": "tsc --pretty --noEmit" }, "husky": { @@ -53,7 +53,7 @@ "idb-keyval": "^5.0.6", "ismobilejs": "^1.1.1", "monaco-editor": "^0.25.2", - "next": "latest", + "next": "^11.0.1", "next-auth": "^3.27.0", "next-pwa": "^5.2.21", "perfect-freehand": "^0.4.9", @@ -66,8 +66,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^13.1.9", + "@babel/core": "^7.14.6", "@types/jest": "^26.0.23", "@types/node": "^14.14.25", "@types/react": "^17.0.1", @@ -80,7 +79,8 @@ "eslint-plugin-react": "^7.19.0", "husky": "^4.2.3", "identity-obj-proxy": "^3.0.0", - "jest": "^27.0.4", + "jest": "^27.0.5", + "jest-esm-transformer": "^1.0.0", "jest-watch-typeahead": "^0.6.1", "lint-staged": "^10.0.10", "prettier": "^2.3.1", diff --git a/scripts/type-gen.js b/scripts/type-gen.js index f441c10bd..96b59273d 100644 --- a/scripts/type-gen.js +++ b/scripts/type-gen.js @@ -1,52 +1,36 @@ // @ts-check /* -Type gen script +This script will generate TypeScript content for the code editor. It inlines +the content of several files into one large string which can be passed to the +Monaco editor as an extraLib. -This script will generate TypeScript declarations for the code editor. It reads -the global types, as well as all of the code classes, and writes them into a -single file as a string. This string is fed into the Monaco editor as an extraLib. +Important notes: + +- Files must include the "Start Copy Here" comment indicated below. + +- This comment must be placed BELOW any import statements. + +Run the script with `yarn scripts`. */ const fs = require('fs/promises') +const root = process.cwd() + +async function inlineFileContents(path) { + console.log(`📄 Inlining contents of ${path}`) + const text = await fs.readFile(`${root}${path}`, 'utf-8') + return text + .match( + /\/\* ----------------- Start Copy Here ---------------- \*\/(.|\n)*$/g + )[0] + .replaceAll('/* ----------------- Start Copy Here ---------------- */', '') + .replaceAll('export default', '') + .replaceAll('export ', '') +} async function copyTypesToFile() { - const types = await fs.readFile(__dirname + '/../types.ts', 'utf8') - const codeIndex = await fs.readFile( - __dirname + '/../state/code/index.ts', - 'utf8' - ) - const codeDot = await fs.readFile(__dirname + '/../state/code/dot.ts', 'utf8') - const codeEllipse = await fs.readFile( - __dirname + '/../state/code/ellipse.ts', - 'utf8' - ) - const codeLine = await fs.readFile( - __dirname + '/../state/code/line.ts', - 'utf8' - ) - const codePolyline = await fs.readFile( - __dirname + '/../state/code/polyline.ts', - 'utf8' - ) - const codeRay = await fs.readFile(__dirname + '/../state/code/ray.ts', 'utf8') - const codeArrow = await fs.readFile( - __dirname + '/../state/code/arrow.ts', - 'utf8' - ) - const codeDraw = await fs.readFile( - __dirname + '/../state/code/draw.ts', - 'utf8' - ) - const codeRectangle = await fs.readFile( - __dirname + '/../state/code/rectangle.ts', - 'utf8' - ) - const codeVector = await fs.readFile(__dirname + '/../utils/vec.ts', 'utf8') - const codeUtils = await fs.readFile( - __dirname + '/../state/code/utils.ts', - 'utf8' - ) + console.log('⚙️ Generating types-import.ts') const content = ` @@ -57,30 +41,44 @@ async function copyTypesToFile() { export default {` + ` name: "types.ts", - content: \` + content: \` + -${types} +${await inlineFileContents('/types.ts')} -${codeIndex.match(/export default(.|\n)*$/g)[0]} -${codeDot.match(/\/\*\*(.|\n)*$/g)[0]} -${codeEllipse.match(/\/\*\*(.|\n)*$/g)[0]} -${codeLine.match(/\/\*\*(.|\n)*$/g)[0]} -${codePolyline.match(/\/\*\*(.|\n)*$/g)[0]} -${codeRay.match(/\/\*\*(.|\n)*$/g)[0]} -${codeRectangle.match(/\/\*\*(.|\n)*$/g)[0]} -${codeArrow.match(/\/\*\*(.|\n)*$/g)[0]} -${codeDraw.match(/\/\*\*(.|\n)*$/g)[0]} -${codeUtils.match(/\/\*\*(.|\n)*$/g)[0]} -${codeVector} -\` - }` - .replaceAll('export default', '') - .replaceAll('export ', '') +${await inlineFileContents('/utils/vec.ts')} + +${await inlineFileContents('/state/code/utils.ts')} + +${await inlineFileContents('/state/code/index.ts')} + +${await inlineFileContents('/state/code/dot.ts')} + +${await inlineFileContents('/state/code/ellipse.ts')} + +${await inlineFileContents('/state/code/line.ts')} + +${await inlineFileContents('/state/code/polyline.ts')} + +${await inlineFileContents('/state/code/ray.ts')} + +${await inlineFileContents('/state/code/arrow.ts')} + +${await inlineFileContents('/state/code/draw.ts')} + +${await inlineFileContents('/state/code/rectangle.ts')} + +${await inlineFileContents('/state/code/control.ts')} + +declare const controls: {[key:string]: any} = {} +\`}` await fs.writeFile( __dirname + '/../components/code-panel/types-import.ts', content ) + + console.log('✅ Process complete') } // Kickoff diff --git a/state/code/arrow.ts b/state/code/arrow.ts index 684dedee8..d99ec6d55 100644 --- a/state/code/arrow.ts +++ b/state/code/arrow.ts @@ -5,9 +5,8 @@ import { defaultStyle } from 'state/shape-styles' import { getShapeUtils } from 'state/shape-utils' import Vec from 'utils/vec' -/** - * ## Draw - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Arrow extends CodeShape { constructor( props = {} as ShapeProps & { start: number[]; end: number[] } diff --git a/state/code/control.ts b/state/code/control.ts index cfa6c48f3..01adb84db 100644 --- a/state/code/control.ts +++ b/state/code/control.ts @@ -10,6 +10,8 @@ export const controls: Record = {} export const codeControls = new Set([]) +/* ----------------- Start Copy Here ---------------- */ + export class Control { control: T @@ -29,14 +31,25 @@ export class Control { codeControls.delete(this.control) delete controls[this.control.label] } + + get value(): T['value'] { + return this.control.value + } + + set value(value: T['value']) { + this.control.value = value + } } +type ControlProps = Omit, 'id' | 'type'> + export class NumberControl extends Control { - constructor(options: Omit) { - const { value = 0, step = 1 } = options + constructor(options: ControlProps) { + const { label = 'Number', value = 0, step = 1 } = options super({ type: ControlType.Number, ...options, + label, value, step, }) @@ -44,11 +57,12 @@ export class NumberControl extends Control { } export class VectorControl extends Control { - constructor(options: Omit) { - const { value = [0, 0], isNormalized = false } = options + constructor(options: ControlProps) { + const { label = 'Vector', value = [0, 0], isNormalized = false } = options super({ type: ControlType.Vector, ...options, + label, value, isNormalized, }) diff --git a/state/code/dot.ts b/state/code/dot.ts index 2cef9af7f..54e60ecd2 100644 --- a/state/code/dot.ts +++ b/state/code/dot.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { DotShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Dot - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Dot extends CodeShape { constructor(props = {} as ShapeProps) { super({ diff --git a/state/code/draw.ts b/state/code/draw.ts index 0c400e78f..3f1dfb78e 100644 --- a/state/code/draw.ts +++ b/state/code/draw.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { DrawShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Draw - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Draw extends CodeShape { constructor(props = {} as ShapeProps) { super({ @@ -13,8 +12,8 @@ export default class Draw extends CodeShape { seed: Math.random(), type: ShapeType.Draw, isGenerated: false, + parentId: (window as any).currentPageId, name: 'Draw', - parentId: 'page1', childIndex: 0, point: [0, 0], points: [], diff --git a/state/code/ellipse.ts b/state/code/ellipse.ts index 79e6fe2ba..c6b3367a2 100644 --- a/state/code/ellipse.ts +++ b/state/code/ellipse.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { EllipseShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Ellipse - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Ellipse extends CodeShape { constructor(props = {} as ShapeProps) { super({ diff --git a/state/code/generate.ts b/state/code/generate.ts index 501e50427..29a13e4c9 100644 --- a/state/code/generate.ts +++ b/state/code/generate.ts @@ -44,13 +44,13 @@ const baseScope = { * collected shapes as an array. * @param code */ -export function generateFromCode( +export async function generateFromCode( data: Data, code: string -): { +): Promise<{ shapes: Shape[] controls: CodeControl[] -} { +}> { codeControls.clear() codeShapes.clear() ;(window as any).isUpdatingCode = false @@ -59,7 +59,9 @@ export function generateFromCode( const { currentPageId } = data const scope = { ...baseScope, controls, currentPageId } - const transformed = transform(code, { transforms: ['typescript'] }).code + const transformed = transform(code, { + transforms: ['typescript'], + }).code new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope)) @@ -87,34 +89,50 @@ export function generateFromCode( * collected shapes as an array. * @param code */ -export function updateFromCode( +export async function updateFromCode( data: Data, code: string -): { +): Promise<{ shapes: Shape[] -} { +}> { codeShapes.clear() ;(window as any).isUpdatingCode = true ;(window as any).currentPageId = data.currentPageId const { currentPageId } = data + const newControls = Object.fromEntries( + Object.entries(data.codeControls).map(([_, control]) => [ + control.label, + control.value, + ]) + ) + const scope = { ...baseScope, currentPageId, - controls: Object.fromEntries( - Object.entries(controls).map(([_, control]) => [ - control.label, - control.value, - ]) - ), + controls: newControls, } - new Function(...Object.keys(scope), `${code}`)(...Object.values(scope)) + const startingChildIndex = + getShapes(data) + .filter((shape) => shape.parentId === data.currentPageId) + .sort((a, b) => a.childIndex - b.childIndex)[0]?.childIndex || 1 - const generatedShapes = Array.from(codeShapes.values()).map( - (instance) => instance.shape - ) + const transformed = transform(code, { + transforms: ['typescript'], + }).code + + new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope)) + + const generatedShapes = Array.from(codeShapes.values()) + .sort((a, b) => a.shape.childIndex - b.shape.childIndex) + .map((instance, i) => ({ + ...instance.shape, + isGenerated: true, + parentId: getPage(data).id, + childIndex: startingChildIndex + i, + })) return { shapes: generatedShapes } } diff --git a/state/code/index.ts b/state/code/index.ts index 59e5dcfb0..dc3089135 100644 --- a/state/code/index.ts +++ b/state/code/index.ts @@ -21,6 +21,9 @@ function getOrderedShapes() { * A base class for code shapes. Note that creating a shape adds it to the * shape map, while deleting it removes it from the collected shapes set */ + +/* ----------------- Start Copy Here ---------------- */ + export default class CodeShape { private _shape: Mutable private utils: ShapeUtility diff --git a/state/code/line.ts b/state/code/line.ts index a973c0162..521ba9e31 100644 --- a/state/code/line.ts +++ b/state/code/line.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { LineShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Line - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Line extends CodeShape { constructor(props = {} as ShapeProps) { super({ diff --git a/state/code/polyline.ts b/state/code/polyline.ts index cac0f3be8..dbef354b4 100644 --- a/state/code/polyline.ts +++ b/state/code/polyline.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { PolylineShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Polyline - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Polyline extends CodeShape { constructor(props = {} as ShapeProps) { super({ @@ -22,8 +21,11 @@ export default class Polyline extends CodeShape { isAspectRatioLocked: false, isLocked: false, isHidden: false, - style: defaultStyle, ...props, + style: { + ...defaultStyle, + ...props.style, + }, }) } diff --git a/state/code/ray.ts b/state/code/ray.ts index 5d655fccc..c1a4ffaa8 100644 --- a/state/code/ray.ts +++ b/state/code/ray.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { RayShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Ray - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Ray extends CodeShape { constructor(props = {} as ShapeProps) { super({ diff --git a/state/code/rectangle.ts b/state/code/rectangle.ts index 7ca834f94..d18e61667 100644 --- a/state/code/rectangle.ts +++ b/state/code/rectangle.ts @@ -3,9 +3,8 @@ import { uniqueId } from 'utils' import { RectangleShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' -/** - * ## Rectangle - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Rectangle extends CodeShape { constructor(props = {} as ShapeProps) { super({ @@ -24,7 +23,10 @@ export default class Rectangle extends CodeShape { isLocked: false, isHidden: false, ...props, - style: { ...defaultStyle, ...props.style }, + style: { + ...defaultStyle, + ...props.style, + }, }) } diff --git a/state/code/utils.ts b/state/code/utils.ts index 8e550c9e2..cf4b882cf 100644 --- a/state/code/utils.ts +++ b/state/code/utils.ts @@ -1,9 +1,8 @@ import { Bounds } from 'types' import vec from 'utils/vec' -/** - * ## Utils - */ +/* ----------------- Start Copy Here ---------------- */ + export default class Utils { /** * Linear interpolation betwen two numbers. diff --git a/state/code/vector.ts b/state/code/vector.ts index 2cd00cd03..97eb46bca 100644 --- a/state/code/vector.ts +++ b/state/code/vector.ts @@ -1,3 +1,5 @@ +/* ----------------- Start Copy Here ---------------- */ + export interface VectorOptions { x: number y: number @@ -8,9 +10,6 @@ export interface Point { y: number } -/** - * ## Vector - */ export default class Vector { x = 0 y = 0 diff --git a/state/state.ts b/state/state.ts index f11ba7adb..70116e6d9 100644 --- a/state/state.ts +++ b/state/state.ts @@ -268,11 +268,11 @@ const state = createState({ do: ['setCodeControls', 'setGeneratedShapes'], }, UNDO: { - unless: 'isInSession', + unless: ['isReadOnly', 'isInSession'], do: 'undo', }, REDO: { - unless: 'isInSession', + unless: ['isReadOnly', 'isInSession'], do: 'redo', }, SAVED: { @@ -1763,12 +1763,10 @@ const state = createState({ setSelectedIds(data, []) try { - const { shapes } = updateFromCode( + updateFromCode( data, data.document.code[data.currentCodeFileId].code - ) - - commands.generate(data, shapes) + ).then(({ shapes }) => commands.generate(data, shapes)) } catch (e) { console.error(e) } diff --git a/types.ts b/types.ts index 921cc1655..1f714ea30 100644 --- a/types.ts +++ b/types.ts @@ -57,6 +57,8 @@ export interface PageState { } } +/* ----------------- Start Copy Here ---------------- */ + export enum ShapeType { Dot = 'dot', Ellipse = 'ellipse', @@ -383,17 +385,20 @@ export interface BaseCodeControl { export interface NumberCodeControl extends BaseCodeControl { type: ControlType.Number + value: number min?: number max?: number - value: number - step: number + step?: number format?: (value: number) => number } export interface VectorCodeControl extends BaseCodeControl { type: ControlType.Vector value: number[] - isNormalized: boolean + min?: number + max?: number + step?: number + isNormalized?: boolean format?: (value: number[]) => number[] } diff --git a/utils/vec.ts b/utils/vec.ts index 2b5b70df7..b9663862c 100644 --- a/utils/vec.ts +++ b/utils/vec.ts @@ -1,5 +1,7 @@ // A big collection of vector utilities. Collected into a class to improve logging / packaging. +/* ----------------- Start Copy Here ---------------- */ + export default class Vec { /** * Clamp a value into a range. diff --git a/yarn.lock b/yarn.lock index 892cf70cf..59daa67ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,7 +21,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.7.tgz#7b047d7a3a89a67d2258dc61f604f098f1bc7e08" integrity sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw== -"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.7.2", "@babel/core@^7.7.5": +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.14.6", "@babel/core@^7.4.4", "@babel/core@^7.7.2", "@babel/core@^7.7.5": version "7.14.6" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.6.tgz#e0814ec1a950032ff16c13a2721de39a8416fcab" integrity sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA== @@ -637,7 +637,7 @@ "@babel/helper-plugin-utils" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.14.5": +"@babel/plugin-transform-modules-commonjs@^7.14.5", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz#7aaee0ea98283de94da98b28f8c35701429dad97" integrity sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A== @@ -1238,25 +1238,25 @@ require_optional "^1.0.1" typeorm "^0.2.30" -"@next/env@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-11.0.0.tgz#bdd306a45e88ba3e4e7a36aa91806f6486bb61d0" - integrity sha512-VKpmDvTYeCpEQjREg3J4pCmVs/QjEzoLmkM8shGFK6e9AmFd0G9QXOL8HGA8qKhy/XmNb7dHeMqrcMiBua4OgA== +"@next/env@11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-11.0.1.tgz#6dc96ac76f1663ab747340e907e8933f190cc8fd" + integrity sha512-yZfKh2U6R9tEYyNUrs2V3SBvCMufkJ07xMH5uWy8wqcl5gAXoEw6A/1LDqwX3j7pUutF9d1ZxpdGDA3Uag+aQQ== "@next/eslint-plugin-next@11.0.0": version "11.0.0" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-11.0.0.tgz#e6fb93a00bdaba371904f2b2698b184e6278d369" integrity sha512-fPZ0904yY1box6bRpR9rJqIkNxJdvzzxH7doXS+cdjyBAdptMR7wj3mcx1hEikBHzWduU8BOXBvRg2hWc09YDQ== -"@next/polyfill-module@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.0.0.tgz#cb2f46b323bbe7f8a337ccd80fb82314d4039403" - integrity sha512-gydtFzRqsT549U8+sY8382I/f4HFcelD8gdUGnAofQJa/jEU1jkxmjCHC8tmEiyeMLidl7iDZgchfSCpmMzzUg== +"@next/polyfill-module@11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.0.1.tgz#ca2a110c1c44672cbcff6c2b983f0c0549d87291" + integrity sha512-Cjs7rrKCg4CF4Jhri8PCKlBXhszTfOQNl9AjzdNy4K5jXFyxyoSzuX2rK4IuoyE+yGp5A3XJCBEmOQ4xbUp9Mg== -"@next/react-dev-overlay@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.0.0.tgz#6befb4d00d952551db1b3909023074eb5778ac5d" - integrity sha512-q+Wp+eStEMThe77zxdeJ/nbuODkHR6P+/dfUqYXZSqbLf6x5c5xwLBauwwVbkCYFZpAlDuL8Jk8QSAH1OsqC2w== +"@next/react-dev-overlay@11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.0.1.tgz#3c481e83347255abd466dcf7e59ac8a79a0d7fd6" + integrity sha512-lvUjMVpLsgzADs9Q8wtC5LNqvfdN+M0BDMSrqr04EDWAyyX0vURHC9hkvLbyEYWyh+WW32pwjKBXdkMnJhoqMg== dependencies: "@babel/code-frame" "7.12.11" anser "1.4.9" @@ -1270,10 +1270,10 @@ stacktrace-parser "0.1.10" strip-ansi "6.0.0" -"@next/react-refresh-utils@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.0.0.tgz#cb671723c50b904eaa44b4b45c0845476ecd8825" - integrity sha512-hi5eY+KBn4QGtUv7VL2OptdM33fI2hxhd7+omOFmAK+S0hDWhg1uqHqqGJk0W1IfqlWEzzL10WvTJDPRAtDugQ== +"@next/react-refresh-utils@11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.0.1.tgz#a7509f22b6f70c13101a26573afd295295f1c020" + integrity sha512-K347DM6Z7gBSE+TfUaTTceWvbj0B6iNAsFZXbFZOlfg3uyz2sbKpzPYYFocCc27yjLaS8OfR8DEdS2mZXi8Saw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1932,45 +1932,11 @@ ejs "^2.6.1" magic-string "^0.25.0" -"@testing-library/dom@^7.28.1": - version "7.31.2" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a" - integrity sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^4.2.2" - chalk "^4.1.0" - dom-accessibility-api "^0.5.6" - lz-string "^1.4.4" - pretty-format "^26.6.2" - -"@testing-library/react@^11.2.5": - version "11.2.7" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.7.tgz#b29e2e95c6765c815786c0bc1d5aed9cb2bf7818" - integrity sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA== - dependencies: - "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^7.28.1" - -"@testing-library/user-event@^13.1.9": - version "13.1.9" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.1.9.tgz#29e49a42659ac3c1023565ff56819e0153a82e99" - integrity sha512-NZr0zL2TMOs2qk+dNlqrAdbaRW5dAmYwd1yuQ4r7HpkVEOj0MWuUjDWwKhcLd/atdBy8ZSMHSKp+kXSQe47ezg== - dependencies: - "@babel/runtime" "^7.12.5" - "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@types/aria-query@^4.2.0": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" - integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== - "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -3422,11 +3388,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6: - version "0.5.6" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" - integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== - domain-browser@4.19.0: version "4.19.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.19.0.tgz#1093e17c0a17dbd521182fe90d49ac1370054af1" @@ -4906,6 +4867,14 @@ jest-environment-node@^27.0.5: jest-mock "^27.0.3" jest-util "^27.0.2" +jest-esm-transformer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jest-esm-transformer/-/jest-esm-transformer-1.0.0.tgz#b6c58f496aa48194f96361a52f5c578fd2209726" + integrity sha512-FoPgeMMwy1/CEsc8tBI41i83CEO3x85RJuZi5iAMmWoARXhfgk6Jd7y+4d+z+HCkTKNVDvSWKGRhwjzU9PUbrw== + dependencies: + "@babel/core" "^7.4.4" + "@babel/plugin-transform-modules-commonjs" "^7.4.4" + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -5210,7 +5179,7 @@ jest-worker@^27.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.0.4: +jest@^27.0.5: version "27.0.5" resolved "https://registry.yarnpkg.com/jest/-/jest-27.0.5.tgz#141825e105514a834cc8d6e44670509e8d74c5f2" integrity sha512-4NlVMS29gE+JOZvgmSAsz3eOjkSsHqjTajlIsah/4MVSmKvf3zFP/TvgcLoWe2UVHiE9KF741sReqhF0p4mqbQ== @@ -5626,11 +5595,6 @@ lru_map@^0.3.3: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= -lz-string@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" - integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= - magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -5814,17 +5778,17 @@ next-pwa@^5.2.21: workbox-webpack-plugin "^6.1.5" workbox-window "^6.1.5" -next@latest: - version "11.0.0" - resolved "https://registry.yarnpkg.com/next/-/next-11.0.0.tgz#866b833f192f5a94ddb3267d5cc0f4b0ce405ac7" - integrity sha512-1OA0ccCTwVtdLats/1v7ReiBVx+Akya0UVhHo9IBr8ZkpDI3/SGNcaruJBp5agy8ROF97VDKkZamoUXxRB9NUA== +next@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/next/-/next-11.0.1.tgz#b8e3914d153aaf7143cb98c09bcd3c8230eeb17a" + integrity sha512-yR7be7asNbvpVNpi6xxEg28wZ7Gqmj1nOt0sABH9qORmF3+pms2KZ7Cng33oK5nqPIzEEFJD0pp2PCe3/ueMIg== dependencies: "@babel/runtime" "7.12.5" "@hapi/accept" "5.0.2" - "@next/env" "11.0.0" - "@next/polyfill-module" "11.0.0" - "@next/react-dev-overlay" "11.0.0" - "@next/react-refresh-utils" "11.0.0" + "@next/env" "11.0.1" + "@next/polyfill-module" "11.0.1" + "@next/react-dev-overlay" "11.0.1" + "@next/react-refresh-utils" "11.0.1" assert "2.0.0" ast-types "0.13.2" browserify-zlib "0.2.0"