Adds error boundary, improves code shapes types.
This commit is contained in:
parent
69bdab520a
commit
32922b3f85
16 changed files with 518 additions and 76 deletions
|
@ -1,5 +1,6 @@
|
||||||
import styled from 'styles'
|
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 React, { useRef } from 'react'
|
||||||
import useZoomEvents from 'hooks/useZoomEvents'
|
import useZoomEvents from 'hooks/useZoomEvents'
|
||||||
import useCamera from 'hooks/useCamera'
|
import useCamera from 'hooks/useCamera'
|
||||||
|
@ -27,6 +28,12 @@ export default function Canvas(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MainSVG ref={rCanvas} {...events}>
|
<MainSVG ref={rCanvas} {...events}>
|
||||||
|
<ErrorBoundary
|
||||||
|
FallbackComponent={ErrorFallback}
|
||||||
|
onReset={() => {
|
||||||
|
// reset the state of your app so the error doesn't happen again
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Defs />
|
<Defs />
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<g ref={rGroup} id="shapes">
|
<g ref={rGroup} id="shapes">
|
||||||
|
@ -37,6 +44,7 @@ export default function Canvas(): JSX.Element {
|
||||||
<Brush />
|
<Brush />
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
</ErrorBoundary>
|
||||||
</MainSVG>
|
</MainSVG>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)
|
)
|
||||||
|
@ -59,3 +67,20 @@ const MainSVG = styled('svg', {
|
||||||
userSelect: 'none',
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@ interface GroupShape extends BaseShape {
|
||||||
size: number[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShapeProps<T extends Shape> = Partial<T> & {
|
type ShapeProps<T extends Shape> = Partial<Omit<T, 'style'>> & {
|
||||||
style?: Partial<ShapeStyles>
|
style?: Partial<ShapeStyles>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,57 +608,235 @@ interface ShapeUtility<K extends Shape> {
|
||||||
return { ...this._shape }
|
return { ...this._shape }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the shape.
|
||||||
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
codeShapes.delete(this)
|
codeShapes.delete(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the shape to a point.
|
||||||
|
* @param delta
|
||||||
|
*/
|
||||||
moveTo(point: number[]): CodeShape<T> {
|
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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(delta: number[]): CodeShape<T> {
|
/**
|
||||||
this.utils.setProperty(
|
* Move the shape by a delta.
|
||||||
this._shape,
|
* @param delta
|
||||||
'point',
|
*/
|
||||||
vec.add(this._shape.point, delta)
|
translateBy(delta: number[]): CodeShape<T> {
|
||||||
)
|
this.utils.translateTo(this._shape, delta)
|
||||||
return this
|
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
|
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> {
|
getBounds(): CodeShape<T> {
|
||||||
this.utils.getBounds(this.shape)
|
this.utils.getBounds(this.shape)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether a point is inside of the shape.
|
||||||
|
*/
|
||||||
hitTest(point: number[]): CodeShape<T> {
|
hitTest(point: number[]): CodeShape<T> {
|
||||||
this.utils.hitTest(this.shape, point)
|
this.utils.hitTest(this.shape, point)
|
||||||
return this
|
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 {
|
get shape(): T {
|
||||||
return this._shape
|
return this._shape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape's current point.
|
||||||
|
*/
|
||||||
get point(): number[] {
|
get point(): number[] {
|
||||||
return [...this.shape.point]
|
return [...this.shape.point]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set point(point: number[]) {
|
||||||
|
getShapeUtils(this.shape).translateTo(this._shape, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape's rotation.
|
||||||
|
*/
|
||||||
get rotation(): number {
|
get rotation(): number {
|
||||||
return this.shape.rotation
|
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
|
* ## Dot
|
||||||
*/
|
*/
|
||||||
class Dot extends CodeShape<DotShape> {
|
class Dot extends CodeShape<DotShape> {
|
||||||
constructor(props = {} as Partial<DotShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<DotShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -686,7 +864,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
* ## Ellipse
|
* ## Ellipse
|
||||||
*/
|
*/
|
||||||
class Ellipse extends CodeShape<EllipseShape> {
|
class Ellipse extends CodeShape<EllipseShape> {
|
||||||
constructor(props = {} as Partial<EllipseShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<EllipseShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -720,7 +898,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
* ## Line
|
* ## Line
|
||||||
*/
|
*/
|
||||||
class Line extends CodeShape<LineShape> {
|
class Line extends CodeShape<LineShape> {
|
||||||
constructor(props = {} as Partial<LineShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<LineShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -753,7 +931,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
* ## Polyline
|
* ## Polyline
|
||||||
*/
|
*/
|
||||||
class Polyline extends CodeShape<PolylineShape> {
|
class Polyline extends CodeShape<PolylineShape> {
|
||||||
constructor(props = {} as Partial<PolylineShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<PolylineShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -782,7 +960,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
* ## Ray
|
* ## Ray
|
||||||
*/
|
*/
|
||||||
class Ray extends CodeShape<RayShape> {
|
class Ray extends CodeShape<RayShape> {
|
||||||
constructor(props = {} as Partial<RayShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<RayShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -846,8 +1024,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
*/
|
*/
|
||||||
class Arrow extends CodeShape<ArrowShape> {
|
class Arrow extends CodeShape<ArrowShape> {
|
||||||
constructor(
|
constructor(
|
||||||
props = {} as Partial<ArrowShape> &
|
props = {} as ShapeProps<ArrowShape> & { start: number[]; end: number[] }
|
||||||
Partial<ShapeStyles> & { start?: number[]; end?: number[] }
|
|
||||||
) {
|
) {
|
||||||
const { start = [0, 0], end = [0, 0] } = props
|
const { start = [0, 0], end = [0, 0] } = props
|
||||||
|
|
||||||
|
@ -940,7 +1117,7 @@ interface ShapeUtility<K extends Shape> {
|
||||||
* ## Draw
|
* ## Draw
|
||||||
*/
|
*/
|
||||||
class Draw extends CodeShape<DrawShape> {
|
class Draw extends CodeShape<DrawShape> {
|
||||||
constructor(props = {} as Partial<DrawShape>) {
|
constructor(props = {} as ShapeProps<DrawShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
@ -1459,12 +1636,24 @@ interface ShapeUtility<K extends Shape> {
|
||||||
return -c / 2 + -step
|
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))
|
return Array.from(Array(steps))
|
||||||
.map((_, i) => {
|
.map((_, i) => ease(i / steps))
|
||||||
const t = i / steps
|
|
||||||
return t * t * t
|
|
||||||
})
|
|
||||||
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
|
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
"perfect-freehand": "^0.4.9",
|
"perfect-freehand": "^0.4.9",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.9",
|
||||||
"react-use-gesture": "^9.1.3",
|
"react-use-gesture": "^9.1.3",
|
||||||
"sucrase": "^3.19.0",
|
"sucrase": "^3.19.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
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 { defaultStyle } from 'state/shape-styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import Vec from 'utils/vec'
|
import Vec from 'utils/vec'
|
||||||
|
@ -10,8 +10,7 @@ import Vec from 'utils/vec'
|
||||||
*/
|
*/
|
||||||
export default class Arrow extends CodeShape<ArrowShape> {
|
export default class Arrow extends CodeShape<ArrowShape> {
|
||||||
constructor(
|
constructor(
|
||||||
props = {} as Partial<ArrowShape> &
|
props = {} as ShapeProps<ArrowShape> & { start: number[]; end: number[] }
|
||||||
Partial<ShapeStyles> & { start?: number[]; end?: number[] }
|
|
||||||
) {
|
) {
|
||||||
const { start = [0, 0], end = [0, 0] } = props
|
const { start = [0, 0], end = [0, 0] } = props
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { DotShape, ShapeStyles, ShapeType } from 'types'
|
import { DotShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Dot
|
* ## Dot
|
||||||
*/
|
*/
|
||||||
export default class Dot extends CodeShape<DotShape> {
|
export default class Dot extends CodeShape<DotShape> {
|
||||||
constructor(props = {} as Partial<DotShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<DotShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { DrawShape, ShapeType } from 'types'
|
import { DrawShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Draw
|
* ## Draw
|
||||||
*/
|
*/
|
||||||
export default class Draw extends CodeShape<DrawShape> {
|
export default class Draw extends CodeShape<DrawShape> {
|
||||||
constructor(props = {} as Partial<DrawShape>) {
|
constructor(props = {} as ShapeProps<DrawShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { EllipseShape, ShapeStyles, ShapeType } from 'types'
|
import { EllipseShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Ellipse
|
* ## Ellipse
|
||||||
*/
|
*/
|
||||||
export default class Ellipse extends CodeShape<EllipseShape> {
|
export default class Ellipse extends CodeShape<EllipseShape> {
|
||||||
constructor(props = {} as Partial<EllipseShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<EllipseShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -10,8 +10,15 @@ import Utils from './utils'
|
||||||
import Vec from 'utils/vec'
|
import Vec from 'utils/vec'
|
||||||
import { NumberControl, VectorControl, codeControls, controls } from './control'
|
import { NumberControl, VectorControl, codeControls, controls } from './control'
|
||||||
import { codeShapes } from './index'
|
import { codeShapes } from './index'
|
||||||
import { CodeControl, Data, Shape } from 'types'
|
import {
|
||||||
import { getPage } from 'utils'
|
CodeControl,
|
||||||
|
Data,
|
||||||
|
Shape,
|
||||||
|
DashStyle,
|
||||||
|
ColorStyle,
|
||||||
|
SizeStyle,
|
||||||
|
} from 'types'
|
||||||
|
import { getPage, getShapes } from 'utils'
|
||||||
import { transform } from 'sucrase'
|
import { transform } from 'sucrase'
|
||||||
|
|
||||||
const baseScope = {
|
const baseScope = {
|
||||||
|
@ -27,6 +34,9 @@ const baseScope = {
|
||||||
Draw,
|
Draw,
|
||||||
VectorControl,
|
VectorControl,
|
||||||
NumberControl,
|
NumberControl,
|
||||||
|
DashStyle,
|
||||||
|
ColorStyle,
|
||||||
|
SizeStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,10 +63,18 @@ export function generateFromCode(
|
||||||
|
|
||||||
new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope))
|
new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope))
|
||||||
|
|
||||||
const generatedShapes = Array.from(codeShapes.values()).map((instance) => ({
|
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,
|
...instance.shape,
|
||||||
isGenerated: true,
|
isGenerated: true,
|
||||||
parentId: getPage(data).id,
|
parentId: getPage(data).id,
|
||||||
|
childIndex: startingChildIndex + i,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const generatedControls = Array.from(codeControls.values())
|
const generatedControls = Array.from(codeControls.values())
|
||||||
|
|
|
@ -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 { createShape, getShapeUtils } from 'state/shape-utils'
|
||||||
import vec from 'utils/vec'
|
import { setToArray } from 'utils'
|
||||||
|
|
||||||
export const codeShapes = new Set<CodeShape<Shape>>([])
|
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
|
* 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
|
* 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 }
|
return { ...this._shape }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the shape.
|
||||||
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
codeShapes.delete(this)
|
codeShapes.delete(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the shape to a point.
|
||||||
|
* @param delta
|
||||||
|
*/
|
||||||
moveTo(point: number[]): CodeShape<T> {
|
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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(delta: number[]): CodeShape<T> {
|
/**
|
||||||
this.utils.setProperty(
|
* Move the shape by a delta.
|
||||||
this._shape,
|
* @param delta
|
||||||
'point',
|
*/
|
||||||
vec.add(this._shape.point, delta)
|
translateBy(delta: number[]): CodeShape<T> {
|
||||||
)
|
this.utils.translateTo(this._shape, delta)
|
||||||
return this
|
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
|
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> {
|
getBounds(): CodeShape<T> {
|
||||||
this.utils.getBounds(this.shape)
|
this.utils.getBounds(this.shape)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether a point is inside of the shape.
|
||||||
|
*/
|
||||||
hitTest(point: number[]): CodeShape<T> {
|
hitTest(point: number[]): CodeShape<T> {
|
||||||
this.utils.hitTest(this.shape, point)
|
this.utils.hitTest(this.shape, point)
|
||||||
return this
|
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 {
|
get shape(): T {
|
||||||
return this._shape
|
return this._shape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape's current point.
|
||||||
|
*/
|
||||||
get point(): number[] {
|
get point(): number[] {
|
||||||
return [...this.shape.point]
|
return [...this.shape.point]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set point(point: number[]) {
|
||||||
|
getShapeUtils(this.shape).translateTo(this._shape, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape's rotation.
|
||||||
|
*/
|
||||||
get rotation(): number {
|
get rotation(): number {
|
||||||
return this.shape.rotation
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { LineShape, ShapeStyles, ShapeType } from 'types'
|
import { LineShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Line
|
* ## Line
|
||||||
*/
|
*/
|
||||||
export default class Line extends CodeShape<LineShape> {
|
export default class Line extends CodeShape<LineShape> {
|
||||||
constructor(props = {} as Partial<LineShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<LineShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { PolylineShape, ShapeStyles, ShapeType } from 'types'
|
import { PolylineShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Polyline
|
* ## Polyline
|
||||||
*/
|
*/
|
||||||
export default class Polyline extends CodeShape<PolylineShape> {
|
export default class Polyline extends CodeShape<PolylineShape> {
|
||||||
constructor(props = {} as Partial<PolylineShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<PolylineShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { RayShape, ShapeStyles, ShapeType } from 'types'
|
import { RayShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Ray
|
* ## Ray
|
||||||
*/
|
*/
|
||||||
export default class Ray extends CodeShape<RayShape> {
|
export default class Ray extends CodeShape<RayShape> {
|
||||||
constructor(props = {} as Partial<RayShape> & Partial<ShapeStyles>) {
|
constructor(props = {} as ShapeProps<RayShape>) {
|
||||||
super({
|
super({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
|
|
|
@ -496,12 +496,24 @@ export default class Utils {
|
||||||
return -c / 2 + -step
|
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))
|
return Array.from(Array(steps))
|
||||||
.map((_, i) => {
|
.map((_, i) => ease(i / steps))
|
||||||
const t = i / steps
|
|
||||||
return t * t * t
|
|
||||||
})
|
|
||||||
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
|
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -335,15 +335,15 @@ class Storage {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const documentName = data.document.name
|
||||||
|
|
||||||
const fa = await import('browser-fs-access')
|
const fa = await import('browser-fs-access')
|
||||||
|
|
||||||
fa.fileSave(
|
fa.fileSave(
|
||||||
blob,
|
blob,
|
||||||
{
|
{
|
||||||
fileName: `${
|
fileName: `${
|
||||||
saveAs
|
saveAs ? documentName : this.previousSaveHandle?.name || 'My Document'
|
||||||
? data.document.name
|
|
||||||
: this.previousSaveHandle?.name || 'My Document'
|
|
||||||
}.tldr`,
|
}.tldr`,
|
||||||
description: 'tldraw file',
|
description: 'tldraw file',
|
||||||
extensions: ['.tldr'],
|
extensions: ['.tldr'],
|
||||||
|
|
2
types.ts
2
types.ts
|
@ -191,7 +191,7 @@ export interface GroupShape extends BaseShape {
|
||||||
size: number[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShapeProps<T extends Shape> = Partial<T> & {
|
export type ShapeProps<T extends Shape> = Partial<Omit<T, 'style'>> & {
|
||||||
style?: Partial<ShapeStyles>
|
style?: Partial<ShapeStyles>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6592,6 +6592,13 @@ react-dom@^17.0.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
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:
|
react-feather@^2.0.9:
|
||||||
version "2.0.9"
|
version "2.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
|
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
|
||||||
|
|
Loading…
Reference in a new issue