From 32922b3f852cce970466850e7aecb8dce73b235c Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 24 Jun 2021 23:09:36 +0100 Subject: [PATCH] Adds error boundary, improves code shapes types. --- components/canvas/canvas.tsx | 47 ++++-- components/code-panel/types-import.ts | 235 +++++++++++++++++++++++--- package.json | 1 + state/code/arrow.ts | 5 +- state/code/dot.ts | 4 +- state/code/draw.ts | 4 +- state/code/ellipse.ts | 4 +- state/code/generate.ts | 32 +++- state/code/index.ts | 213 +++++++++++++++++++++-- state/code/line.ts | 4 +- state/code/polyline.ts | 4 +- state/code/ray.ts | 4 +- state/code/utils.ts | 22 ++- state/storage.ts | 6 +- types.ts | 2 +- yarn.lock | 7 + 16 files changed, 518 insertions(+), 76 deletions(-) diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx index 3e7e30290..12d74c24f 100644 --- a/components/canvas/canvas.tsx +++ b/components/canvas/canvas.tsx @@ -1,5 +1,6 @@ import styled from 'styles' -import { useSelector } from 'state' +import { ErrorBoundary } from 'react-error-boundary' +import state, { useSelector } from 'state' import React, { useRef } from 'react' import useZoomEvents from 'hooks/useZoomEvents' import useCamera from 'hooks/useCamera' @@ -27,16 +28,23 @@ export default function Canvas(): JSX.Element { return ( - - {isReady && ( - - - - - - - - )} + { + // reset the state of your app so the error doesn't happen again + }} + > + + {isReady && ( + + + + + + + + )} + ) @@ -59,3 +67,20 @@ const MainSVG = styled('svg', { userSelect: 'none', }, }) + +function ErrorFallback({ error, resetErrorBoundary }) { + React.useEffect(() => { + console.error(error) + const copy = 'Sorry, something went wrong. Clear canvas and continue?' + if (window.confirm(copy)) { + state.send('CLEARED_PAGE') + resetErrorBoundary() + } + }, []) + + return ( + + Oops + + ) +} diff --git a/components/code-panel/types-import.ts b/components/code-panel/types-import.ts index 3ba44582e..9d9c525cb 100644 --- a/components/code-panel/types-import.ts +++ b/components/code-panel/types-import.ts @@ -199,7 +199,7 @@ interface GroupShape extends BaseShape { size: number[] } -type ShapeProps = Partial & { +type ShapeProps = Partial> & { style?: Partial } @@ -608,57 +608,235 @@ interface ShapeUtility { return { ...this._shape } } + /** + * Destroy the shape. + */ destroy(): void { codeShapes.delete(this) } + /** + * Move the shape to a point. + * @param delta + */ moveTo(point: number[]): CodeShape { - this.utils.setProperty(this._shape, 'point', point) + return this.translateTo(point) + } + + /** + * Move the shape to a point. + * @param delta + */ + translateTo(point: number[]): CodeShape { + this.utils.translateTo(this._shape, point) return this } - translate(delta: number[]): CodeShape { - this.utils.setProperty( - this._shape, - 'point', - vec.add(this._shape.point, delta) - ) + /** + * Move the shape by a delta. + * @param delta + */ + translateBy(delta: number[]): CodeShape { + this.utils.translateTo(this._shape, delta) return this } - rotate(rotation: number): CodeShape { - this.utils.setProperty(this._shape, 'rotation', rotation) + /** + * Rotate the shape. + */ + rotateTo(rotation: number): CodeShape { + this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation) return this } + /** + * Rotate the shape by a delta. + */ + rotateBy(rotation: number): CodeShape { + this.utils.rotateBy(this._shape, rotation) + return this + } + + /** + * Get the shape's bounding box. + */ getBounds(): CodeShape { this.utils.getBounds(this.shape) return this } + /** + * Test whether a point is inside of the shape. + */ hitTest(point: number[]): CodeShape { this.utils.hitTest(this.shape, point) return this } + /** + * Move the shape to the back of the painting order. + */ + 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 + } + + /** + * Move the shape to the top of the painting order. + */ + 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 + } + + /** + * Move the shape backward in the painting order. + */ + 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 + } + + /** + * Move the shape forward in the painting order. + */ + 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 + } + + /** + * The shape's underlying shape. + */ get shape(): T { return this._shape } + /** + * The shape's current point. + */ get point(): number[] { return [...this.shape.point] } + set point(point: number[]) { + getShapeUtils(this.shape).translateTo(this._shape, point) + } + + /** + * The shape's rotation. + */ get rotation(): number { return this.shape.rotation } + + set rotation(rotation: number) { + getShapeUtils(this.shape).rotateTo( + this._shape, + rotation, + rotation - this.shape.rotation + ) + } + + /** + * The shape's color style. + */ + get color(): ColorStyle { + return this.shape.style.color + } + + set color(color: ColorStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { color }) + } + + /** + * The shape's dash style. + */ + get dash(): DashStyle { + return this.shape.style.dash + } + + set dash(dash: DashStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { dash }) + } + + /** + * The shape's stroke width. + */ + get strokeWidth(): SizeStyle { + return this.shape.style.size + } + + set strokeWidth(size: SizeStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { size }) + } + + /** + * The shape's index in the painting order. + */ + get childIndex(): number { + return this.shape.childIndex + } + + set childIndex(childIndex: number) { + getShapeUtils(this.shape).setProperty(this._shape, 'childIndex', childIndex) + } } /** * ## Dot */ class Dot extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -686,7 +864,7 @@ interface ShapeUtility { * ## Ellipse */ class Ellipse extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -720,7 +898,7 @@ interface ShapeUtility { * ## Line */ class Line extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -753,7 +931,7 @@ interface ShapeUtility { * ## Polyline */ class Polyline extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -782,7 +960,7 @@ interface ShapeUtility { * ## Ray */ class Ray extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -846,8 +1024,7 @@ interface ShapeUtility { */ class Arrow extends CodeShape { constructor( - props = {} as Partial & - Partial & { start?: number[]; end?: number[] } + props = {} as ShapeProps & { start: number[]; end: number[] } ) { const { start = [0, 0], end = [0, 0] } = props @@ -940,7 +1117,7 @@ interface ShapeUtility { * ## Draw */ class Draw extends CodeShape { - constructor(props = {} as Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), @@ -1459,12 +1636,24 @@ interface ShapeUtility { return -c / 2 + -step } - static getPointsBetween(a: number[], b: number[], steps = 6): number[][] { + /** + * Get an array of points between two points. + * @param a + * @param b + * @param options + */ + static getPointsBetween( + a: number[], + b: number[], + options = {} as { + steps?: number + ease?: (t: number) => number + } + ): number[][] { + const { steps = 6, ease = (t) => t * t * t } = options + return Array.from(Array(steps)) - .map((_, i) => { - const t = i / steps - return t * t * t - }) + .map((_, i) => ease(i / steps)) .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2]) } diff --git a/package.json b/package.json index c2c47b641..b61b2ef80 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "perfect-freehand": "^0.4.9", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-error-boundary": "^3.1.3", "react-feather": "^2.0.9", "react-use-gesture": "^9.1.3", "sucrase": "^3.19.0", diff --git a/state/code/arrow.ts b/state/code/arrow.ts index 8345db485..684dedee8 100644 --- a/state/code/arrow.ts +++ b/state/code/arrow.ts @@ -1,6 +1,6 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { ArrowShape, Decoration, ShapeStyles, ShapeType } from 'types' +import { ArrowShape, Decoration, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' import { getShapeUtils } from 'state/shape-utils' import Vec from 'utils/vec' @@ -10,8 +10,7 @@ import Vec from 'utils/vec' */ export default class Arrow extends CodeShape { constructor( - props = {} as Partial & - Partial & { start?: number[]; end?: number[] } + props = {} as ShapeProps & { start: number[]; end: number[] } ) { const { start = [0, 0], end = [0, 0] } = props diff --git a/state/code/dot.ts b/state/code/dot.ts index c7395a8f2..2cef9af7f 100644 --- a/state/code/dot.ts +++ b/state/code/dot.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { DotShape, ShapeStyles, ShapeType } from 'types' +import { DotShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Dot */ export default class Dot extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/draw.ts b/state/code/draw.ts index 7b00a59a6..0c400e78f 100644 --- a/state/code/draw.ts +++ b/state/code/draw.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { DrawShape, ShapeType } from 'types' +import { DrawShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Draw */ export default class Draw extends CodeShape { - constructor(props = {} as Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/ellipse.ts b/state/code/ellipse.ts index 4774b7fce..79e6fe2ba 100644 --- a/state/code/ellipse.ts +++ b/state/code/ellipse.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { EllipseShape, ShapeStyles, ShapeType } from 'types' +import { EllipseShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Ellipse */ export default class Ellipse extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/generate.ts b/state/code/generate.ts index f934fb5a6..501e50427 100644 --- a/state/code/generate.ts +++ b/state/code/generate.ts @@ -10,8 +10,15 @@ import Utils from './utils' import Vec from 'utils/vec' import { NumberControl, VectorControl, codeControls, controls } from './control' import { codeShapes } from './index' -import { CodeControl, Data, Shape } from 'types' -import { getPage } from 'utils' +import { + CodeControl, + Data, + Shape, + DashStyle, + ColorStyle, + SizeStyle, +} from 'types' +import { getPage, getShapes } from 'utils' import { transform } from 'sucrase' const baseScope = { @@ -27,6 +34,9 @@ const baseScope = { Draw, VectorControl, NumberControl, + DashStyle, + ColorStyle, + SizeStyle, } /** @@ -53,11 +63,19 @@ export function generateFromCode( new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope)) - const generatedShapes = Array.from(codeShapes.values()).map((instance) => ({ - ...instance.shape, - isGenerated: true, - parentId: getPage(data).id, - })) + 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()) + .sort((a, b) => a.shape.childIndex - b.shape.childIndex) + .map((instance, i) => ({ + ...instance.shape, + isGenerated: true, + parentId: getPage(data).id, + childIndex: startingChildIndex + i, + })) const generatedControls = Array.from(codeControls.values()) diff --git a/state/code/index.ts b/state/code/index.ts index a5ba8bc3e..59e5dcfb0 100644 --- a/state/code/index.ts +++ b/state/code/index.ts @@ -1,9 +1,22 @@ -import { Mutable, Shape, ShapeUtility } from 'types' +import { + ColorStyle, + DashStyle, + Mutable, + Shape, + ShapeUtility, + SizeStyle, +} from 'types' import { createShape, getShapeUtils } from 'state/shape-utils' -import vec from 'utils/vec' +import { setToArray } from 'utils' export const codeShapes = new Set>([]) +function getOrderedShapes() { + return setToArray(codeShapes).sort( + (a, b) => a.shape.childIndex - b.shape.childIndex + ) +} + /** * 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 @@ -22,48 +35,226 @@ export default class CodeShape { return { ...this._shape } } + /** + * Destroy the shape. + */ destroy(): void { codeShapes.delete(this) } + /** + * Move the shape to a point. + * @param delta + */ moveTo(point: number[]): CodeShape { - this.utils.setProperty(this._shape, 'point', point) + return this.translateTo(point) + } + + /** + * Move the shape to a point. + * @param delta + */ + translateTo(point: number[]): CodeShape { + this.utils.translateTo(this._shape, point) return this } - translate(delta: number[]): CodeShape { - this.utils.setProperty( - this._shape, - 'point', - vec.add(this._shape.point, delta) - ) + /** + * Move the shape by a delta. + * @param delta + */ + translateBy(delta: number[]): CodeShape { + this.utils.translateTo(this._shape, delta) return this } - rotate(rotation: number): CodeShape { - this.utils.setProperty(this._shape, 'rotation', rotation) + /** + * Rotate the shape. + */ + rotateTo(rotation: number): CodeShape { + this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation) return this } + /** + * Rotate the shape by a delta. + */ + rotateBy(rotation: number): CodeShape { + this.utils.rotateBy(this._shape, rotation) + return this + } + + /** + * Get the shape's bounding box. + */ getBounds(): CodeShape { this.utils.getBounds(this.shape) return this } + /** + * Test whether a point is inside of the shape. + */ hitTest(point: number[]): CodeShape { this.utils.hitTest(this.shape, point) return this } + /** + * Move the shape to the back of the painting order. + */ + 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 + } + + /** + * Move the shape to the top of the painting order. + */ + 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 + } + + /** + * Move the shape backward in the painting order. + */ + 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 + } + + /** + * Move the shape forward in the painting order. + */ + 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 + } + + /** + * The shape's underlying shape. + */ get shape(): T { return this._shape } + /** + * The shape's current point. + */ get point(): number[] { return [...this.shape.point] } + set point(point: number[]) { + getShapeUtils(this.shape).translateTo(this._shape, point) + } + + /** + * The shape's rotation. + */ get rotation(): number { return this.shape.rotation } + + set rotation(rotation: number) { + getShapeUtils(this.shape).rotateTo( + this._shape, + rotation, + rotation - this.shape.rotation + ) + } + + /** + * The shape's color style. + */ + get color(): ColorStyle { + return this.shape.style.color + } + + set color(color: ColorStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { color }) + } + + /** + * The shape's dash style. + */ + get dash(): DashStyle { + return this.shape.style.dash + } + + set dash(dash: DashStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { dash }) + } + + /** + * The shape's stroke width. + */ + get strokeWidth(): SizeStyle { + return this.shape.style.size + } + + set strokeWidth(size: SizeStyle) { + getShapeUtils(this.shape).applyStyles(this._shape, { size }) + } + + /** + * The shape's index in the painting order. + */ + get childIndex(): number { + return this.shape.childIndex + } + + set childIndex(childIndex: number) { + getShapeUtils(this.shape).setProperty(this._shape, 'childIndex', childIndex) + } } diff --git a/state/code/line.ts b/state/code/line.ts index c53e6ee19..a973c0162 100644 --- a/state/code/line.ts +++ b/state/code/line.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { LineShape, ShapeStyles, ShapeType } from 'types' +import { LineShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Line */ export default class Line extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/polyline.ts b/state/code/polyline.ts index 0e7d9c883..cac0f3be8 100644 --- a/state/code/polyline.ts +++ b/state/code/polyline.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { PolylineShape, ShapeStyles, ShapeType } from 'types' +import { PolylineShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Polyline */ export default class Polyline extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/ray.ts b/state/code/ray.ts index 1f561230d..5d655fccc 100644 --- a/state/code/ray.ts +++ b/state/code/ray.ts @@ -1,13 +1,13 @@ import CodeShape from './index' import { uniqueId } from 'utils' -import { RayShape, ShapeStyles, ShapeType } from 'types' +import { RayShape, ShapeProps, ShapeType } from 'types' import { defaultStyle } from 'state/shape-styles' /** * ## Ray */ export default class Ray extends CodeShape { - constructor(props = {} as Partial & Partial) { + constructor(props = {} as ShapeProps) { super({ id: uniqueId(), seed: Math.random(), diff --git a/state/code/utils.ts b/state/code/utils.ts index e62385788..8e550c9e2 100644 --- a/state/code/utils.ts +++ b/state/code/utils.ts @@ -496,12 +496,24 @@ export default class Utils { return -c / 2 + -step } - static getPointsBetween(a: number[], b: number[], steps = 6): number[][] { + /** + * Get an array of points between two points. + * @param a + * @param b + * @param options + */ + static getPointsBetween( + a: number[], + b: number[], + options = {} as { + steps?: number + ease?: (t: number) => number + } + ): number[][] { + const { steps = 6, ease = (t) => t * t * t } = options + return Array.from(Array(steps)) - .map((_, i) => { - const t = i / steps - return t * t * t - }) + .map((_, i) => ease(i / steps)) .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2]) } diff --git a/state/storage.ts b/state/storage.ts index 87eeaa1e3..57d25a31f 100644 --- a/state/storage.ts +++ b/state/storage.ts @@ -335,15 +335,15 @@ class Storage { } ) + const documentName = data.document.name + const fa = await import('browser-fs-access') fa.fileSave( blob, { fileName: `${ - saveAs - ? data.document.name - : this.previousSaveHandle?.name || 'My Document' + saveAs ? documentName : this.previousSaveHandle?.name || 'My Document' }.tldr`, description: 'tldraw file', extensions: ['.tldr'], diff --git a/types.ts b/types.ts index 49ca10354..921cc1655 100644 --- a/types.ts +++ b/types.ts @@ -191,7 +191,7 @@ export interface GroupShape extends BaseShape { size: number[] } -export type ShapeProps = Partial & { +export type ShapeProps = Partial> & { style?: Partial } diff --git a/yarn.lock b/yarn.lock index cb713e58e..892cf70cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6592,6 +6592,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b" + integrity sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA== + dependencies: + "@babel/runtime" "^7.12.5" + react-feather@^2.0.9: version "2.0.9" resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"