Adds error boundary, improves code shapes types.

This commit is contained in:
Steve Ruiz 2021-06-24 23:09:36 +01:00
parent 69bdab520a
commit 32922b3f85
16 changed files with 518 additions and 76 deletions

View file

@ -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 (
<ContextMenu>
<MainSVG ref={rCanvas} {...events}>
<Defs />
{isReady && (
<g ref={rGroup} id="shapes">
<BoundsBg />
<Page />
<Bounds />
<Handles />
<Brush />
</g>
)}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<Defs />
{isReady && (
<g ref={rGroup} id="shapes">
<BoundsBg />
<Page />
<Bounds />
<Handles />
<Brush />
</g>
)}
</ErrorBoundary>
</MainSVG>
</ContextMenu>
)
@ -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 (
<g>
<text>Oops</text>
</g>
)
}

View file

@ -199,7 +199,7 @@ interface GroupShape extends BaseShape {
size: number[]
}
type ShapeProps<T extends Shape> = Partial<T> & {
type ShapeProps<T extends Shape> = Partial<Omit<T, 'style'>> & {
style?: Partial<ShapeStyles>
}
@ -608,57 +608,235 @@ interface ShapeUtility<K extends Shape> {
return { ...this._shape }
}
/**
* Destroy the shape.
*/
destroy(): void {
codeShapes.delete(this)
}
/**
* Move the shape to a point.
* @param delta
*/
moveTo(point: number[]): CodeShape<T> {
this.utils.setProperty(this._shape, 'point', point)
return this.translateTo(point)
}
/**
* Move the shape to a point.
* @param delta
*/
translateTo(point: number[]): CodeShape<T> {
this.utils.translateTo(this._shape, point)
return this
}
translate(delta: number[]): CodeShape<T> {
this.utils.setProperty(
this._shape,
'point',
vec.add(this._shape.point, delta)
)
/**
* Move the shape by a delta.
* @param delta
*/
translateBy(delta: number[]): CodeShape<T> {
this.utils.translateTo(this._shape, delta)
return this
}
rotate(rotation: number): CodeShape<T> {
this.utils.setProperty(this._shape, 'rotation', rotation)
/**
* Rotate the shape.
*/
rotateTo(rotation: number): CodeShape<T> {
this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation)
return this
}
/**
* Rotate the shape by a delta.
*/
rotateBy(rotation: number): CodeShape<T> {
this.utils.rotateBy(this._shape, rotation)
return this
}
/**
* Get the shape's bounding box.
*/
getBounds(): CodeShape<T> {
this.utils.getBounds(this.shape)
return this
}
/**
* Test whether a point is inside of the shape.
*/
hitTest(point: number[]): CodeShape<T> {
this.utils.hitTest(this.shape, point)
return this
}
/**
* Move the shape to the back of the painting order.
*/
moveToBack(): CodeShape<T> {
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<T> {
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<T> {
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<T> {
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<DotShape> {
constructor(props = {} as Partial<DotShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<DotShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -686,7 +864,7 @@ interface ShapeUtility<K extends Shape> {
* ## Ellipse
*/
class Ellipse extends CodeShape<EllipseShape> {
constructor(props = {} as Partial<EllipseShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<EllipseShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -720,7 +898,7 @@ interface ShapeUtility<K extends Shape> {
* ## Line
*/
class Line extends CodeShape<LineShape> {
constructor(props = {} as Partial<LineShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<LineShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -753,7 +931,7 @@ interface ShapeUtility<K extends Shape> {
* ## Polyline
*/
class Polyline extends CodeShape<PolylineShape> {
constructor(props = {} as Partial<PolylineShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<PolylineShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -782,7 +960,7 @@ interface ShapeUtility<K extends Shape> {
* ## Ray
*/
class Ray extends CodeShape<RayShape> {
constructor(props = {} as Partial<RayShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<RayShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -846,8 +1024,7 @@ interface ShapeUtility<K extends Shape> {
*/
class Arrow extends CodeShape<ArrowShape> {
constructor(
props = {} as Partial<ArrowShape> &
Partial<ShapeStyles> & { start?: number[]; end?: number[] }
props = {} as ShapeProps<ArrowShape> & { start: number[]; end: number[] }
) {
const { start = [0, 0], end = [0, 0] } = props
@ -940,7 +1117,7 @@ interface ShapeUtility<K extends Shape> {
* ## Draw
*/
class Draw extends CodeShape<DrawShape> {
constructor(props = {} as Partial<DrawShape>) {
constructor(props = {} as ShapeProps<DrawShape>) {
super({
id: uniqueId(),
seed: Math.random(),
@ -1459,12 +1636,24 @@ interface ShapeUtility<K extends Shape> {
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])
}

View file

@ -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",

View file

@ -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<ArrowShape> {
constructor(
props = {} as Partial<ArrowShape> &
Partial<ShapeStyles> & { start?: number[]; end?: number[] }
props = {} as ShapeProps<ArrowShape> & { start: number[]; end: number[] }
) {
const { start = [0, 0], end = [0, 0] } = props

View file

@ -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<DotShape> {
constructor(props = {} as Partial<DotShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<DotShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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<DrawShape> {
constructor(props = {} as Partial<DrawShape>) {
constructor(props = {} as ShapeProps<DrawShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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<EllipseShape> {
constructor(props = {} as Partial<EllipseShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<EllipseShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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())

View file

@ -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<CodeShape<Shape>>([])
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<T extends Shape> {
return { ...this._shape }
}
/**
* Destroy the shape.
*/
destroy(): void {
codeShapes.delete(this)
}
/**
* Move the shape to a point.
* @param delta
*/
moveTo(point: number[]): CodeShape<T> {
this.utils.setProperty(this._shape, 'point', point)
return this.translateTo(point)
}
/**
* Move the shape to a point.
* @param delta
*/
translateTo(point: number[]): CodeShape<T> {
this.utils.translateTo(this._shape, point)
return this
}
translate(delta: number[]): CodeShape<T> {
this.utils.setProperty(
this._shape,
'point',
vec.add(this._shape.point, delta)
)
/**
* Move the shape by a delta.
* @param delta
*/
translateBy(delta: number[]): CodeShape<T> {
this.utils.translateTo(this._shape, delta)
return this
}
rotate(rotation: number): CodeShape<T> {
this.utils.setProperty(this._shape, 'rotation', rotation)
/**
* Rotate the shape.
*/
rotateTo(rotation: number): CodeShape<T> {
this.utils.rotateTo(this._shape, rotation, this.shape.rotation - rotation)
return this
}
/**
* Rotate the shape by a delta.
*/
rotateBy(rotation: number): CodeShape<T> {
this.utils.rotateBy(this._shape, rotation)
return this
}
/**
* Get the shape's bounding box.
*/
getBounds(): CodeShape<T> {
this.utils.getBounds(this.shape)
return this
}
/**
* Test whether a point is inside of the shape.
*/
hitTest(point: number[]): CodeShape<T> {
this.utils.hitTest(this.shape, point)
return this
}
/**
* Move the shape to the back of the painting order.
*/
moveToBack(): CodeShape<T> {
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<T> {
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<T> {
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<T> {
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)
}
}

View file

@ -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<LineShape> {
constructor(props = {} as Partial<LineShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<LineShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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<PolylineShape> {
constructor(props = {} as Partial<PolylineShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<PolylineShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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<RayShape> {
constructor(props = {} as Partial<RayShape> & Partial<ShapeStyles>) {
constructor(props = {} as ShapeProps<RayShape>) {
super({
id: uniqueId(),
seed: Math.random(),

View file

@ -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])
}

View file

@ -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'],

View file

@ -191,7 +191,7 @@ export interface GroupShape extends BaseShape {
size: number[]
}
export type ShapeProps<T extends Shape> = Partial<T> & {
export type ShapeProps<T extends Shape> = Partial<Omit<T, 'style'>> & {
style?: Partial<ShapeStyles>
}

View file

@ -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"