Improves status, create session handling

This commit is contained in:
Steve Ruiz 2021-08-15 09:49:38 +01:00
parent ffc180fa1c
commit 40cbf2d92b
15 changed files with 478 additions and 266 deletions

View file

@ -3,16 +3,19 @@ import { useTLDrawContext } from '~hooks'
import type { Data } from '~types' import type { Data } from '~types'
import styled from '~styles' import styled from '~styles'
const statusSelector = (s: Data) => s.appState.status.current
const activeToolSelector = (s: Data) => s.appState.activeTool const activeToolSelector = (s: Data) => s.appState.activeTool
export function StatusBar(): JSX.Element | null { export function StatusBar(): JSX.Element | null {
const { useSelector } = useTLDrawContext() const { useSelector } = useTLDrawContext()
const status = useSelector(statusSelector)
const activeTool = useSelector(activeToolSelector) const activeTool = useSelector(activeToolSelector)
return ( return (
<StatusBarContainer size={{ '@sm': 'small' }}> <StatusBarContainer size={{ '@sm': 'small' }}>
<Section>{activeTool}</Section> <Section>
{/* <Section>{shapesInView || '0'} Shapes</Section> */} {activeTool} | {status}
</Section>
</StatusBarContainer> </StatusBarContainer>
) )
} }

View file

@ -8,7 +8,6 @@ import {
Intersect, Intersect,
TLHandle, TLHandle,
TLPointerInfo, TLPointerInfo,
Svg,
} from '@tldraw/core' } from '@tldraw/core'
import getStroke from 'perfect-freehand' import getStroke from 'perfect-freehand'
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles' import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
@ -27,7 +26,7 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
type = TLDrawShapeType.Arrow as const type = TLDrawShapeType.Arrow as const
toolType = TLDrawToolType.Handle toolType = TLDrawToolType.Handle
canStyleFill = false canStyleFill = false
simplePathCache = new WeakMap<ArrowShape, string>() simplePathCache = new WeakMap<ArrowShape['handles'], string>()
pathCache = new WeakMap<ArrowShape, string>() pathCache = new WeakMap<ArrowShape, string>()
defaultProps = { defaultProps = {
@ -72,12 +71,63 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
} }
render = (shape: ArrowShape, { isDarkMode }: TLRenderInfo) => { render = (shape: ArrowShape, { isDarkMode }: TLRenderInfo) => {
const { bend, handles, style } = shape const {
const { start, end, bend: _bend } = handles handles: { start, bend, end },
decorations = {},
style,
} = shape
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1 const isDraw = style.dash === DashStyle.Draw
const isDraw = shape.style.dash === DashStyle.Draw // if (!isDraw) {
// const styles = getShapeStyle(style, isDarkMode)
// const { strokeWidth } = styles
// const arrowDist = Vec.dist(start.point, end.point)
// const sw = strokeWidth * 1.618
// const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
// arrowDist,
// sw,
// shape.style.dash,
// 2
// )
// const path = getArrowPath(shape)
// return (
// <g pointerEvents="none">
// <path
// d={path}
// fill="none"
// stroke="transparent"
// strokeWidth={Math.max(8, strokeWidth * 2)}
// strokeDasharray="none"
// strokeDashoffset="none"
// strokeLinecap="round"
// strokeLinejoin="round"
// pointerEvents="stroke"
// />
// <path
// d={path}
// fill={isDraw ? styles.stroke : 'none'}
// stroke={styles.stroke}
// strokeWidth={sw}
// strokeDasharray={strokeDasharray}
// strokeDashoffset={strokeDashoffset}
// strokeLinecap="round"
// strokeLinejoin="round"
// pointerEvents="stroke"
// />
// </g>
// )
// }
// TODO: Improve drawn arrows
const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
const styles = getShapeStyle(style, isDarkMode) const styles = getShapeStyle(style, isDarkMode)
@ -85,11 +135,11 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
const arrowDist = Vec.dist(start.point, end.point) const arrowDist = Vec.dist(start.point, end.point)
const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8) const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
let shaftPath: JSX.Element let shaftPath: JSX.Element
let insetStart: number[] let startArrowHead: { left: number[]; right: number[] } | undefined
let insetEnd: number[] let endArrowHead: { left: number[]; right: number[] } | undefined
if (isStraightLine) { if (isStraightLine) {
const sw = strokeWidth * (isDraw ? 0.618 : 1.618) const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
@ -107,8 +157,13 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
2 2
) )
insetStart = Vec.nudge(start.point, end.point, arrowHeadlength) if (decorations.start) {
insetEnd = Vec.nudge(end.point, start.point, arrowHeadlength) startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength)
}
if (decorations.end) {
endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength)
}
// Straight arrow path // Straight arrow path
shaftPath = ( shaftPath = (
@ -144,31 +199,37 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
const path = Utils.getFromCache(this.pathCache, shape, () => const path = Utils.getFromCache(this.pathCache, shape, () =>
isDraw isDraw
? renderCurvedFreehandArrowShaft(shape, circle) ? renderCurvedFreehandArrowShaft(shape, circle)
: getArrowArcPath(start, end, circle, bend) : getArrowArcPath(start, end, circle, shape.bend)
) )
const arcLength = Utils.getArcLength( const { center, radius, length } = getArrowArc(shape)
[circle[0], circle[1]],
circle[2],
start.point,
end.point
)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
arcLength - 1, length - 1,
sw, sw,
shape.style.dash, shape.style.dash,
2 2
) )
const center = [circle[0], circle[1]] if (decorations.start) {
const radius = circle[2] startArrowHead = getCurvedArrowHeadPoints(
const sa = Vec.angle(center, start.point) start.point,
const ea = Vec.angle(center, end.point) arrowHeadLength,
const t = arrowHeadlength / Math.abs(arcLength) center,
radius,
length < 0
)
}
insetStart = Vec.nudgeAtAngle(center, Utils.lerpAngles(sa, ea, t), radius) if (decorations.end) {
insetEnd = Vec.nudgeAtAngle(center, Utils.lerpAngles(ea, sa, t), radius) endArrowHead = getCurvedArrowHeadPoints(
end.point,
arrowHeadLength,
center,
radius,
length >= 0
)
}
// Curved arrow path // Curved arrow path
shaftPath = ( shaftPath = (
@ -204,9 +265,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
return ( return (
<g pointerEvents="none"> <g pointerEvents="none">
{shaftPath} {shaftPath}
{shape.decorations?.start === Decoration.Arrow && ( {startArrowHead && (
<path <path
d={getArrowHeadPath(shape, start.point, insetStart)} d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
fill="none" fill="none"
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={sw}
@ -217,9 +278,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
pointerEvents="stroke" pointerEvents="stroke"
/> />
)} )}
{shape.decorations?.end === Decoration.Arrow && ( {endArrowHead && (
<path <path
d={getArrowHeadPath(shape, end.point, insetEnd)} d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
fill="none" fill="none"
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={sw}
@ -235,91 +296,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
} }
renderIndicator(shape: ArrowShape) { renderIndicator(shape: ArrowShape) {
const { const path = Utils.getFromCache(this.simplePathCache, shape.handles, () => getArrowPath(shape))
decorations,
handles: { start, end, bend: _bend },
style,
} = shape
const { strokeWidth } = getShapeStyle(style, false) return <path d={path} />
const arrowDist = Vec.dist(start.point, end.point)
const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
const aw = arrowHeadlength / 2
const path: (string | number)[] = []
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
if (isStraightLine) {
path.push(Svg.moveTo(start.point), Svg.lineTo(end.point))
if (decorations?.start) {
const point = start.point
const ints = Intersect.circle.lineSegment(start.point, aw, start.point, end.point).points
const int = ints[0]
path.push(
Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
Svg.lineTo(start.point),
Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
)
}
if (decorations?.end) {
const point = end.point
const ints = Intersect.circle.lineSegment(end.point, aw, start.point, end.point).points
const int = ints[0]
path.push(
Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
Svg.lineTo(end.point),
Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
)
}
} else {
const circle = getCtp(shape)
const center = [circle[0], circle[1]]
const radius = circle[2]
const sweep = Utils.getArcLength(center, radius, start.point, end.point) > 0
path.push(
Svg.moveTo(start.point),
`A ${radius} ${radius} 0 0 ${sweep ? '1' : '0'} ${end.point}`
)
if (decorations?.start) {
const point = start.point
const ints = Intersect.circle.circle(center, radius, point, aw).points
const int = sweep ? ints[0] : ints[1]
path.push(
Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
Svg.lineTo(start.point),
Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
)
}
if (decorations?.end) {
const point = end.point
const ints = Intersect.circle.circle(center, radius, point, aw).points
const int = sweep ? ints[1] : ints[0]
path.push(
Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
Svg.lineTo(end.point),
Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
)
}
}
return (
<g>
<path d={path.join()} />
</g>
)
} }
getBounds = (shape: ArrowShape) => { getBounds = (shape: ArrowShape) => {
@ -764,3 +743,143 @@ function getCtp(shape: ArrowShape) {
const { start, end, bend } = shape.handles const { start, end, bend } = shape.handles
return Utils.circleFromThreePoints(start.point, end.point, bend.point) return Utils.circleFromThreePoints(start.point, end.point, bend.point)
} }
function getArrowArc(shape: ArrowShape) {
const { start, end, bend } = shape.handles
const [cx, cy, radius] = Utils.circleFromThreePoints(start.point, end.point, bend.point)
const center = [cx, cy]
const length = Utils.getArcLength(center, radius, start.point, end.point)
return { center, radius, length }
}
function getCurvedArrowHeadPoints(
A: number[],
r1: number,
C: number[],
r2: number,
sweep: boolean
) {
const ints = Intersect.circle.circle(A, r1 * 0.618, C, r2).points
const int = sweep ? ints[0] : ints[1]
const left = Vec.nudge(Vec.rotWith(int, A, Math.PI / 6), A, r1 * -0.382)
const right = Vec.nudge(Vec.rotWith(int, A, -Math.PI / 6), A, r1 * -0.382)
return { left, right }
}
function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
const ints = Intersect.circle.lineSegment(A, r, A, B).points
const int = ints[0]
const left = Vec.rotWith(int, A, Math.PI / 6)
const right = Vec.rotWith(int, A, -Math.PI / 6)
return { left, right }
}
function getCurvedArrowHeadPath(A: number[], r1: number, C: number[], r2: number, sweep: boolean) {
const { left, right } = getCurvedArrowHeadPoints(A, r1, C, r2, sweep)
return `M ${left} L ${A} ${right}`
}
function getStraightArrowHeadPath(A: number[], B: number[], r: number) {
const { left, right } = getStraightArrowHeadPoints(A, B, r)
return `M ${left} L ${A} ${right}`
}
function getArrowPath(shape: ArrowShape) {
const {
decorations,
handles: { start, end, bend: _bend },
style,
} = shape
const { strokeWidth } = getShapeStyle(style, false)
const arrowDist = Vec.dist(start.point, end.point)
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
const path: (string | number)[] = []
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
if (isStraightLine) {
// Path (line segment)
path.push(`M ${start.point} L ${end.point}`)
// Start arrow head
if (decorations?.start) {
path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
}
// End arrow head
if (decorations?.end) {
path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
}
} else {
const { center, radius, length } = getArrowArc(shape)
// Path (arc)
path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
// Start Arrow head
if (decorations?.start) {
path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
}
// End arrow head
if (decorations?.end) {
path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
}
}
return path.join(' ')
}
function getDrawArrowPath(shape: ArrowShape) {
const {
decorations,
handles: { start, end, bend: _bend },
style,
} = shape
const { strokeWidth } = getShapeStyle(style, false)
const arrowDist = Vec.dist(start.point, end.point)
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
const path: (string | number)[] = []
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
if (isStraightLine) {
// Path (line segment)
path.push(`M ${start.point} L ${end.point}`)
// Start arrow head
if (decorations?.start) {
path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
}
// End arrow head
if (decorations?.end) {
path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
}
} else {
const { center, radius, length } = getArrowArc(shape)
// Path (arc)
path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
// Start Arrow head
if (decorations?.start) {
path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
}
// End arrow head
if (decorations?.end) {
path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
}
}
return path.join(' ')
}

View file

@ -45,11 +45,11 @@ describe('Move command', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b']) tlstate.setSelectedIds(['b'])
tlstate.moveToBack() tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.getState())).toBe('bacd') expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
tlstate.undo() tlstate.undo()
expect(getSortedShapeIds(tlstate.getState())).toBe('abcd') expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
tlstate.redo() tlstate.redo()
expect(getSortedShapeIds(tlstate.getState())).toBe('bacd') expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
}) })
describe('to back', () => { describe('to back', () => {
@ -57,21 +57,21 @@ describe('Move command', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b']) tlstate.setSelectedIds(['b'])
tlstate.moveToBack() tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.getState())).toBe('bacd') expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
}) })
it('moves two adjacent siblings to back', () => { it('moves two adjacent siblings to back', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b', 'c']) tlstate.setSelectedIds(['b', 'c'])
tlstate.moveToBack() tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.getState())).toBe('bcad') expect(getSortedShapeIds(tlstate.data)).toBe('bcad')
}) })
it('moves two non-adjacent siblings to back', () => { it('moves two non-adjacent siblings to back', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b', 'd']) tlstate.setSelectedIds(['b', 'd'])
tlstate.moveToBack() tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.getState())).toBe('bdac') expect(getSortedShapeIds(tlstate.data)).toBe('bdac')
}) })
}) })
@ -80,35 +80,35 @@ describe('Move command', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['c']) tlstate.setSelectedIds(['c'])
tlstate.moveBackward() tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.getState())).toBe('acbd') expect(getSortedShapeIds(tlstate.data)).toBe('acbd')
}) })
it('moves a shape at first index backward', () => { it('moves a shape at first index backward', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a']) tlstate.setSelectedIds(['a'])
tlstate.moveBackward() tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.getState())).toBe('abcd') expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
}) })
it('moves two adjacent siblings backward', () => { it('moves two adjacent siblings backward', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['c', 'd']) tlstate.setSelectedIds(['c', 'd'])
tlstate.moveBackward() tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.getState())).toBe('acdb') expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
}) })
it('moves two non-adjacent siblings backward', () => { it('moves two non-adjacent siblings backward', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b', 'd']) tlstate.setSelectedIds(['b', 'd'])
tlstate.moveBackward() tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.getState())).toBe('badc') expect(getSortedShapeIds(tlstate.data)).toBe('badc')
}) })
it('moves two adjacent siblings backward at zero index', () => { it('moves two adjacent siblings backward at zero index', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a', 'b']) tlstate.setSelectedIds(['a', 'b'])
tlstate.moveBackward() tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.getState())).toBe('abcd') expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
}) })
}) })
@ -117,7 +117,7 @@ describe('Move command', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['c']) tlstate.setSelectedIds(['c'])
tlstate.moveForward() tlstate.moveForward()
expect(getSortedShapeIds(tlstate.getState())).toBe('abdc') expect(getSortedShapeIds(tlstate.data)).toBe('abdc')
}) })
it('moves a shape forward at the top index', () => { it('moves a shape forward at the top index', () => {
@ -126,28 +126,28 @@ describe('Move command', () => {
tlstate.moveForward() tlstate.moveForward()
tlstate.moveForward() tlstate.moveForward()
tlstate.moveForward() tlstate.moveForward()
expect(getSortedShapeIds(tlstate.getState())).toBe('acdb') expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
}) })
it('moves two adjacent siblings forward', () => { it('moves two adjacent siblings forward', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a', 'b']) tlstate.setSelectedIds(['a', 'b'])
tlstate.moveForward() tlstate.moveForward()
expect(getSortedShapeIds(tlstate.getState())).toBe('cabd') expect(getSortedShapeIds(tlstate.data)).toBe('cabd')
}) })
it('moves two non-adjacent siblings forward', () => { it('moves two non-adjacent siblings forward', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a', 'c']) tlstate.setSelectedIds(['a', 'c'])
tlstate.moveForward() tlstate.moveForward()
expect(getSortedShapeIds(tlstate.getState())).toBe('badc') expect(getSortedShapeIds(tlstate.data)).toBe('badc')
}) })
it('moves two adjacent siblings forward at top index', () => { it('moves two adjacent siblings forward at top index', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['c', 'd']) tlstate.setSelectedIds(['c', 'd'])
tlstate.moveForward() tlstate.moveForward()
expect(getSortedShapeIds(tlstate.getState())).toBe('abcd') expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
}) })
}) })
@ -156,28 +156,28 @@ describe('Move command', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['b']) tlstate.setSelectedIds(['b'])
tlstate.moveToFront() tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.getState())).toBe('acdb') expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
}) })
it('moves two adjacent siblings to front', () => { it('moves two adjacent siblings to front', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a', 'b']) tlstate.setSelectedIds(['a', 'b'])
tlstate.moveToFront() tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.getState())).toBe('cdab') expect(getSortedShapeIds(tlstate.data)).toBe('cdab')
}) })
it('moves two non-adjacent siblings to front', () => { it('moves two non-adjacent siblings to front', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['a', 'c']) tlstate.setSelectedIds(['a', 'c'])
tlstate.moveToFront() tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.getState())).toBe('bdac') expect(getSortedShapeIds(tlstate.data)).toBe('bdac')
}) })
it('moves siblings already at front to front', () => { it('moves siblings already at front to front', () => {
tlstate.loadDocument(doc) tlstate.loadDocument(doc)
tlstate.setSelectedIds(['c', 'd']) tlstate.setSelectedIds(['c', 'd'])
tlstate.moveToFront() tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.getState())).toBe('abcd') expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
}) })
}) })
}) })

View file

@ -33,3 +33,13 @@ When we mutate shapes inside of a command, we:
- When the history "does" the command, merge the "redo" data into the current `Data`. - When the history "does" the command, merge the "redo" data into the current `Data`.
- When the history "undoes" the command, merge the "undo" data into the current `Data`. - When the history "undoes" the command, merge the "undo" data into the current `Data`.
- When the history "redoes" the command, merge the "redo" data into the current `Data`. - When the history "redoes" the command, merge the "redo" data into the current `Data`.
## onChange Events
When something changes in the state, we need to produce an onChange event that is compatible with multiplayer implementations. This still requires some research, however at minimum we want to include:
- The current user's id
- The current document id
- The event patch (what's changed)
The first step would be to implement onChange events for commands. These are already set up as patches and always produce a history entry.

View file

@ -1,9 +1,18 @@
import type { ArrowBinding, ArrowShape, TLDrawShape, TLDrawBinding, Data, Session } from '~types' import {
ArrowBinding,
ArrowShape,
TLDrawShape,
TLDrawBinding,
Data,
Session,
TLDrawStatus,
} from '~types'
import { Vec, Utils } from '@tldraw/core' import { Vec, Utils } from '@tldraw/core'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class ArrowSession implements Session { export class ArrowSession implements Session {
id = 'transform_single' id = 'transform_single'
status = TLDrawStatus.TranslatingHandle
newBindingId = Utils.uniqueId() newBindingId = Utils.uniqueId()
delta = [0, 0] delta = [0, 0]
offset = [0, 0] offset = [0, 0]

View file

@ -1,10 +1,11 @@
import { brushUpdater, Utils, Vec } from '@tldraw/core' import { brushUpdater, Utils, Vec } from '@tldraw/core'
import type { Data, Session } from '~types' import { Data, Session, TLDrawStatus } from '~types'
import { getShapeUtils } from '~shape' import { getShapeUtils } from '~shape'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class BrushSession implements Session { export class BrushSession implements Session {
id = 'brush' id = 'brush'
status = TLDrawStatus.Brushing
origin: number[] origin: number[]
snapshot: BrushSnapshot snapshot: BrushSnapshot

View file

@ -1,9 +1,10 @@
import { Utils, Vec } from '@tldraw/core' import { Utils, Vec } from '@tldraw/core'
import type { Data, DrawShape, Session } from '~types' import { Data, DrawShape, Session, TLDrawStatus } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class DrawSession implements Session { export class DrawSession implements Session {
id = 'draw' id = 'draw'
status = TLDrawStatus.Creating
origin: number[] origin: number[]
previous: number[] previous: number[]
last: number[] last: number[]

View file

@ -1,11 +1,12 @@
import { Vec } from '@tldraw/core' import { Vec } from '@tldraw/core'
import type { ShapesWithProp } from '~types' import { ShapesWithProp, TLDrawStatus } from '~types'
import type { Session } from '~types' import type { Session } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class HandleSession implements Session { export class HandleSession implements Session {
id = 'transform_single' id = 'transform_single'
status = TLDrawStatus.TranslatingHandle
commandId: string commandId: string
delta = [0, 0] delta = [0, 0]
origin: number[] origin: number[]

View file

@ -1,5 +1,5 @@
import { Utils, Vec } from '@tldraw/core' import { Utils, Vec } from '@tldraw/core'
import type { Session } from '~types' import { Session, TLDrawStatus } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
@ -7,6 +7,7 @@ const PI2 = Math.PI * 2
export class RotateSession implements Session { export class RotateSession implements Session {
id = 'rotate' id = 'rotate'
status = TLDrawStatus.Transforming
delta = [0, 0] delta = [0, 0]
origin: number[] origin: number[]
snapshot: RotateSnapshot snapshot: RotateSnapshot

View file

@ -1,10 +1,11 @@
import type { TextShape } from '~types' import { TextShape, TLDrawStatus } from '~types'
import type { Session } from '~types' import type { Session } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class TextSession implements Session { export class TextSession implements Session {
id = 'text' id = 'text'
status = TLDrawStatus.EditingText
initialShape: TextShape initialShape: TextShape
constructor(data: Data, id?: string) { constructor(data: Data, id?: string) {

View file

@ -1,11 +1,12 @@
import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core' import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
import type { TLDrawShape } from '~types' import { TLDrawShape, TLDrawStatus } from '~types'
import type { Session } from '~types' import type { Session } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class TransformSingleSession implements Session { export class TransformSingleSession implements Session {
id = 'transform_single' id = 'transform_single'
status = TLDrawStatus.Transforming
commandId: string commandId: string
transformType: TLBoundsEdge | TLBoundsCorner transformType: TLBoundsEdge | TLBoundsCorner
origin: number[] origin: number[]

View file

@ -1,10 +1,11 @@
import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core' import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
import type { Session } from '~types' import { Session, TLDrawStatus } from '~types'
import type { Data } from '~types' import type { Data } from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class TransformSession implements Session { export class TransformSession implements Session {
id = 'transform' id = 'transform'
status = TLDrawStatus.Transforming
scaleX = 1 scaleX = 1
scaleY = 1 scaleY = 1
transformType: TLBoundsEdge | TLBoundsCorner transformType: TLBoundsEdge | TLBoundsCorner

View file

@ -1,9 +1,18 @@
import { Utils, Vec } from '@tldraw/core' import { Utils, Vec } from '@tldraw/core'
import type { TLDrawShape, TLDrawBinding, PagePartial, Session, Data, Command } from '~types' import {
TLDrawShape,
TLDrawBinding,
PagePartial,
Session,
Data,
Command,
TLDrawStatus,
} from '~types'
import { TLDR } from '~state/tldr' import { TLDR } from '~state/tldr'
export class TranslateSession implements Session { export class TranslateSession implements Session {
id = 'translate' id = 'translate'
status = TLDrawStatus.Translating
delta = [0, 0] delta = [0, 0]
prev = [0, 0] prev = [0, 0]
origin: number[] origin: number[]

View file

@ -62,6 +62,10 @@ const initialData: Data = {
isToolLocked: false, isToolLocked: false,
isStyleOpen: false, isStyleOpen: false,
isEmptyCanvas: false, isEmptyCanvas: false,
status: {
current: TLDrawStatus.Idle,
previous: TLDrawStatus.Idle,
},
}, },
page: { page: {
id: 'page', id: 'page',
@ -87,10 +91,6 @@ export class TLDrawState implements TLCallbacks {
} }
clipboard?: TLDrawShape[] clipboard?: TLDrawShape[]
session?: Session session?: Session
status: { current: TLDrawStatus; previous: TLDrawStatus } = {
current: 'idle',
previous: 'idle',
}
pointedId?: string pointedId?: string
pointedHandle?: string pointedHandle?: string
editingId?: string editingId?: string
@ -99,12 +99,28 @@ export class TLDrawState implements TLCallbacks {
currentPageId = 'page' currentPageId = 'page'
pages: Record<string, TLPage<TLDrawShape, TLDrawBinding>> = { page: initialData.page } pages: Record<string, TLPage<TLDrawShape, TLDrawBinding>> = { page: initialData.page }
pageStates: Record<string, TLPageState> = { page: initialData.pageState } pageStates: Record<string, TLPageState> = { page: initialData.pageState }
isCreating = false
_onChange?: (state: TLDrawState, reason: string) => void _onChange?: (state: TLDrawState, reason: string) => void
// Low API // Low API
getState = this.store.getState private getState = this.store.getState
setState = <T extends keyof Data>(data: Partial<Data> | ((data: Data) => Partial<Data>)) => { private setStatus(status: TLDrawStatus) {
this.store.setState((state) => ({
appState: {
...state.appState,
status: {
current: status,
previous: state.appState.status.current,
},
},
}))
}
private setState = <T extends keyof Data>(
data: Partial<Data> | ((data: Data) => Partial<Data>),
status?: TLDrawStatus
) => {
const current = this.getState() const current = this.getState()
// Apply incoming change // Apply incoming change
@ -190,6 +206,16 @@ export class TLDrawState implements TLCallbacks {
} }
} }
if (status) {
next.appState = {
...next.appState,
status: {
current: status,
previous: next.appState.status.current,
},
}
}
this.store.setState(next as PartialState<Data, T, T, T>) this.store.setState(next as PartialState<Data, T, T, T>)
this.pages[next.page.id] = next.page this.pages[next.page.id] = next.page
this.pageStates[next.page.id] = next.pageState this.pageStates[next.page.id] = next.pageState
@ -249,12 +275,12 @@ export class TLDrawState implements TLCallbacks {
return this return this
} }
/* --------------------- Status --------------------- */ /* --------------------- Status --------------------- */
setStatus(status: TLDrawStatus) { // setStatus(status: TLDrawStatus) {
this.status.previous = this.status.current // this.status.previous = this.status.current
this.status.current = status // this.status.current = status
// console.log(this.status.previous, ' -> ', this.status.current) // // console.log(this.status.previous, ' -> ', this.status.current)
return this // return this
} // }
/* -------------------- App State ------------------- */ /* -------------------- App State ------------------- */
reset = () => { reset = () => {
this.setState((data) => ({ this.setState((data) => ({
@ -525,7 +551,7 @@ export class TLDrawState implements TLCallbacks {
/* -------------------- Sessions -------------------- */ /* -------------------- Sessions -------------------- */
startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) { startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) {
this.session = session this.session = session
this.setState((data) => session.start(data, ...args)) this.setState((data) => session.start(data, ...args), session.status)
this._onChange?.(this, `session:start_${session.id}`) this._onChange?.(this, `session:start_${session.id}`)
return this return this
} }
@ -542,10 +568,13 @@ export class TLDrawState implements TLCallbacks {
const { session } = this const { session } = this
if (!session) return this if (!session) return this
this.setState((data) => session.cancel(data, ...args)) this.setState((data) => session.cancel(data, ...args), TLDrawStatus.Idle)
this.setStatus('idle')
this.session = undefined this.session = undefined
this.isCreating = false
this._onChange?.(this, `session:cancel:${session.id}`) this._onChange?.(this, `session:cancel:${session.id}`)
return this return this
} }
@ -553,16 +582,9 @@ export class TLDrawState implements TLCallbacks {
const { session } = this const { session } = this
if (!session) return this if (!session) return this
this.setStatus('idle') const current = this.getState()
const result = session.complete(this.store.getState(), ...args) const result = session.complete(current, ...args)
if ('after' in result) {
this.do(result)
} else {
this.setState((data) => Utils.deepMerge<Data>(data, result))
this._onChange?.(this, `session:complete:${session.id}`)
}
const { isToolLocked, activeTool } = this.appState const { isToolLocked, activeTool } = this.appState
@ -571,6 +593,35 @@ export class TLDrawState implements TLCallbacks {
} }
this.session = undefined this.session = undefined
if ('after' in result) {
// Session ended with a command
if (this.isCreating) {
// We're currently creating a shape. Override the command's
// before state so that when we undo the command, we remove
// the shape we just created.
result.before = {
page: {
shapes: Object.fromEntries(current.pageState.selectedIds.map((id) => [id, undefined])),
},
pageState: {
selectedIds: [],
editingId: undefined,
bindingId: undefined,
hoveredId: undefined,
},
}
}
this.isCreating = false
this.do(result)
} else {
this.setState((data) => Utils.deepMerge<Data>(data, result), TLDrawStatus.Idle)
this._onChange?.(this, `session:complete:${session.id}`)
}
return this return this
} }
@ -586,12 +637,14 @@ export class TLDrawState implements TLCallbacks {
history.pointer = history.stack.length - 1 history.pointer = history.stack.length - 1
this.setState((data) => this.setState(
Object.fromEntries( (data) =>
Object.entries(command.after).map(([key, partial]) => { Object.fromEntries(
return [key, Utils.deepMerge(data[key as keyof Data], partial)] Object.entries(command.after).map(([key, partial]) => {
}) return [key, Utils.deepMerge(data[key as keyof Data], partial)]
) })
),
TLDrawStatus.Idle
) )
this._onChange?.(this, `command:${command.id}`) this._onChange?.(this, `command:${command.id}`)
@ -606,12 +659,14 @@ export class TLDrawState implements TLCallbacks {
const command = history.stack[history.pointer] const command = history.stack[history.pointer]
this.setState((data) => this.setState(
Object.fromEntries( (data) =>
Object.entries(command.before).map(([key, partial]) => { Object.fromEntries(
return [key, Utils.deepMerge(data[key as keyof Data], partial)] Object.entries(command.before).map(([key, partial]) => {
}) return [key, Utils.deepMerge(data[key as keyof Data], partial)]
) })
),
TLDrawStatus.Idle
) )
history.pointer-- history.pointer--
@ -630,13 +685,16 @@ export class TLDrawState implements TLCallbacks {
const command = history.stack[history.pointer] const command = history.stack[history.pointer]
this.setState((data) => this.setState(
Object.fromEntries( (data) =>
Object.entries(command.after).map(([key, partial]) => { Object.fromEntries(
return [key, Utils.deepMerge(data[key as keyof Data], partial)] Object.entries(command.after).map(([key, partial]) => {
}) return [key, Utils.deepMerge(data[key as keyof Data], partial)]
) })
),
TLDrawStatus.Idle
) )
this._onChange?.(this, `redo:${command.id}`) this._onChange?.(this, `redo:${command.id}`)
return this return this
@ -834,7 +892,14 @@ export class TLDrawState implements TLCallbacks {
} }
cancel = () => { cancel = () => {
switch (this.status.current) { if (this.isCreating) {
this.cancelSession()
this.delete()
this.isCreating = false
return
}
switch (this.appState.status.current) {
case 'idle': { case 'idle': {
this.deselectAll() this.deselectAll()
this.selectTool('select') this.selectTool('select')
@ -857,11 +922,6 @@ export class TLDrawState implements TLCallbacks {
this.cancelSession() this.cancelSession()
break break
} }
case 'creating': {
this.cancelSession()
this.delete()
break
}
} }
return this return this
@ -968,7 +1028,6 @@ export class TLDrawState implements TLCallbacks {
} }
/* -------------------- Sessions -------------------- */ /* -------------------- Sessions -------------------- */
startBrushSession = (point: number[]) => { startBrushSession = (point: number[]) => {
this.setStatus('brushing')
this.startSession(new Sessions.BrushSession(this.store.getState(), point)) this.startSession(new Sessions.BrushSession(this.store.getState(), point))
return this return this
} }
@ -979,7 +1038,6 @@ export class TLDrawState implements TLCallbacks {
} }
startTranslateSession = (point: number[]) => { startTranslateSession = (point: number[]) => {
this.setStatus('translating')
this.startSession(new Sessions.TranslateSession(this.store.getState(), point)) this.startSession(new Sessions.TranslateSession(this.store.getState(), point))
return this return this
} }
@ -998,8 +1056,6 @@ export class TLDrawState implements TLCallbacks {
if (selectedIds.length === 0) return this if (selectedIds.length === 0) return this
this.setStatus('transforming')
this.pointedBoundsHandle = handle this.pointedBoundsHandle = handle
if (this.pointedBoundsHandle === 'rotate') { if (this.pointedBoundsHandle === 'rotate') {
@ -1032,7 +1088,6 @@ export class TLDrawState implements TLCallbacks {
startTextSession = (id?: string) => { startTextSession = (id?: string) => {
this.editingId = id this.editingId = id
this.setStatus('editing-text')
this.startSession(new Sessions.TextSession(this.store.getState(), id)) this.startSession(new Sessions.TextSession(this.store.getState(), id))
return this return this
} }
@ -1043,7 +1098,6 @@ export class TLDrawState implements TLCallbacks {
} }
startDrawSession = (id: string, point: number[]) => { startDrawSession = (id: string, point: number[]) => {
this.setStatus('creating')
this.startSession(new Sessions.DrawSession(this.store.getState(), id, point)) this.startSession(new Sessions.DrawSession(this.store.getState(), id, point))
return this return this
} }
@ -1077,44 +1131,41 @@ export class TLDrawState implements TLCallbacks {
return this return this
} }
updatenPointerMove: TLPointerEventHandler = (info) => { updateOnPointerMove: TLPointerEventHandler = (info) => {
switch (this.status.current) { switch (this.status.current) {
case 'pointingBoundsHandle': { case TLDrawStatus.PointingBoundsHandle: {
if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle') if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')
if (Vec.dist(info.origin, info.point) > 4) { if (Vec.dist(info.origin, info.point) > 4) {
this.setStatus('transforming')
this.startTransformSession(this.getPagePoint(info.origin), this.pointedBoundsHandle) this.startTransformSession(this.getPagePoint(info.origin), this.pointedBoundsHandle)
} }
break break
} }
case 'pointingHandle': { case TLDrawStatus.PointingHandle: {
if (!this.pointedHandle) throw Error('No pointed handle') if (!this.pointedHandle) throw Error('No pointed handle')
if (Vec.dist(info.origin, info.point) > 4) { if (Vec.dist(info.origin, info.point) > 4) {
this.setStatus('translatingHandle')
this.startHandleSession(this.getPagePoint(info.origin), this.pointedHandle) this.startHandleSession(this.getPagePoint(info.origin), this.pointedHandle)
} }
break break
} }
case 'pointingBounds': { case TLDrawStatus.PointingBounds: {
if (Vec.dist(info.origin, info.point) > 4) { if (Vec.dist(info.origin, info.point) > 4) {
this.setStatus('translating')
this.startTranslateSession(this.getPagePoint(info.origin)) this.startTranslateSession(this.getPagePoint(info.origin))
} }
break break
} }
case 'brushing': { case TLDrawStatus.Brushing: {
this.updateBrushSession(this.getPagePoint(info.point), info.metaKey) this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
break break
} }
case 'translating': { case TLDrawStatus.Translating: {
this.updateTranslateSession(this.getPagePoint(info.point), info.shiftKey, info.altKey) this.updateTranslateSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
break break
} }
case 'transforming': { case TLDrawStatus.Transforming: {
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey) this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
break break
} }
case 'translatingHandle': { case TLDrawStatus.TranslatingHandle: {
this.updateHandleSession( this.updateHandleSession(
this.getPagePoint(info.point), this.getPagePoint(info.point),
info.shiftKey, info.shiftKey,
@ -1123,7 +1174,7 @@ export class TLDrawState implements TLCallbacks {
) )
break break
} }
case 'creating': { case TLDrawStatus.Creating: {
switch (this.appState.activeToolType) { switch (this.appState.activeToolType) {
case 'draw': { case 'draw': {
this.updateDrawSession(this.getPagePoint(info.point), info.pressure, info.shiftKey) this.updateDrawSession(this.getPagePoint(info.point), info.pressure, info.shiftKey)
@ -1193,7 +1244,9 @@ export class TLDrawState implements TLCallbacks {
selectedIds: [id], selectedIds: [id],
}, },
} }
}) }, TLDrawStatus.Creating)
this.isCreating = true
const { activeTool, activeToolType } = this.getAppState() const { activeTool, activeToolType } = this.getAppState()
@ -1231,10 +1284,10 @@ export class TLDrawState implements TLCallbacks {
} }
switch (this.status.current) { switch (this.status.current) {
case 'idle': { case TLDrawStatus.Idle: {
break break
} }
case 'brushing': { case TLDrawStatus.Brushing: {
if (key === 'Meta' || key === 'Control') { if (key === 'Meta' || key === 'Control') {
this.updateBrushSession(this.getPagePoint(info.point), info.metaKey) this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
return return
@ -1242,7 +1295,7 @@ export class TLDrawState implements TLCallbacks {
break break
} }
case 'translating': { case TLDrawStatus.Translating: {
if (key === 'Escape') { if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point)) this.cancelSession(this.getPagePoint(info.point))
} }
@ -1252,7 +1305,7 @@ export class TLDrawState implements TLCallbacks {
} }
break break
} }
case 'transforming': { case TLDrawStatus.Transforming: {
if (key === 'Escape') { if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point)) this.cancelSession(this.getPagePoint(info.point))
} }
@ -1262,7 +1315,7 @@ export class TLDrawState implements TLCallbacks {
} }
break break
} }
case 'translatingHandle': { case TLDrawStatus.TranslatingHandle: {
if (key === 'Escape') { if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point)) this.cancelSession(this.getPagePoint(info.point))
} }
@ -1282,25 +1335,25 @@ export class TLDrawState implements TLCallbacks {
onKeyUp = (key: string, info: TLKeyboardInfo) => { onKeyUp = (key: string, info: TLKeyboardInfo) => {
switch (this.status.current) { switch (this.status.current) {
case 'brushing': { case TLDrawStatus.Brushing: {
if (key === 'Meta' || key === 'Control') { if (key === 'Meta' || key === 'Control') {
this.updateBrushSession(this.getPagePoint(info.point), info.metaKey) this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
} }
break break
} }
case 'transforming': { case TLDrawStatus.Transforming: {
if (key === 'Shift' || key === 'Alt') { if (key === 'Shift' || key === 'Alt') {
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey) this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
} }
break break
} }
case 'translating': { case TLDrawStatus.Translating: {
if (key === 'Shift' || key === 'Alt') { if (key === 'Shift' || key === 'Alt') {
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey) this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
} }
break break
} }
case 'translatingHandle': { case TLDrawStatus.TranslatingHandle: {
if (key === 'Escape') { if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point)) this.cancelSession(this.getPagePoint(info.point))
} }
@ -1320,7 +1373,7 @@ export class TLDrawState implements TLCallbacks {
/* ------------- Renderer Event Handlers ------------ */ /* ------------- Renderer Event Handlers ------------ */
onPinchStart: TLPinchEventHandler = () => { onPinchStart: TLPinchEventHandler = () => {
this.setStatus('pinching') this.setStatus(TLDrawStatus.Pinching)
} }
onPinchEnd: TLPinchEventHandler = () => { onPinchEnd: TLPinchEventHandler = () => {
@ -1328,14 +1381,14 @@ export class TLDrawState implements TLCallbacks {
} }
onPinch: TLPinchEventHandler = (info, e) => { onPinch: TLPinchEventHandler = (info, e) => {
if (this.status.current !== 'pinching') return if (this.status.current !== TLDrawStatus.Pinching) return
this.pinchZoom(info.origin, info.delta, info.delta[2] / 350) this.pinchZoom(info.origin, info.delta, info.delta[2] / 350)
this.updatenPointerMove(info, e as any) this.updateOnPointerMove(info, e as any)
} }
onPan: TLWheelEventHandler = (info, e) => { onPan: TLWheelEventHandler = (info, e) => {
if (this.status.current === 'pinching') return if (this.status.current === TLDrawStatus.Pinching) return
// TODO: Pan and pinchzoom are firing at the same time. Considering turning one of them off! // TODO: Pan and pinchzoom are firing at the same time. Considering turning one of them off!
const delta = Vec.div(info.delta, this.getPageState().camera.zoom) const delta = Vec.div(info.delta, this.getPageState().camera.zoom)
@ -1345,36 +1398,32 @@ export class TLDrawState implements TLCallbacks {
if (Vec.isEqual(next, prev)) return if (Vec.isEqual(next, prev)) return
this.pan(delta) this.pan(delta)
this.updatenPointerMove(info, e as any) this.updateOnPointerMove(info, e as any)
} }
onZoom: TLWheelEventHandler = (info, e) => { onZoom: TLWheelEventHandler = (info, e) => {
this.zoom(info.delta[2] / 100) this.zoom(info.delta[2] / 100)
this.updatenPointerMove(info, e as any) this.updateOnPointerMove(info, e as any)
} }
// Pointer Events // Pointer Events
onPointerDown: TLPointerEventHandler = (info) => { onPointerDown: TLPointerEventHandler = (info) => {
switch (this.status.current) { switch (this.status.current) {
case 'idle': { case TLDrawStatus.Idle: {
switch (this.appState.activeTool) { switch (this.appState.activeTool) {
case 'draw': { case 'draw': {
this.setStatus('creating')
this.createActiveToolShape(info.point) this.createActiveToolShape(info.point)
break break
} }
case 'rectangle': { case 'rectangle': {
this.setStatus('creating')
this.createActiveToolShape(info.point) this.createActiveToolShape(info.point)
break break
} }
case 'ellipse': { case 'ellipse': {
this.setStatus('creating')
this.createActiveToolShape(info.point) this.createActiveToolShape(info.point)
break break
} }
case 'arrow': { case 'arrow': {
this.setStatus('creating')
this.createActiveToolShape(info.point) this.createActiveToolShape(info.point)
break break
} }
@ -1384,14 +1433,14 @@ export class TLDrawState implements TLCallbacks {
} }
onPointerMove: TLPointerEventHandler = (info, e) => { onPointerMove: TLPointerEventHandler = (info, e) => {
this.updatenPointerMove(info, e) this.updateOnPointerMove(info, e)
} }
onPointerUp: TLPointerEventHandler = (info) => { onPointerUp: TLPointerEventHandler = (info) => {
const data = this.getState() const data = this.getState()
switch (this.status.current) { switch (this.status.current) {
case 'pointingBounds': { case TLDrawStatus.PointingBounds: {
if (info.target === 'bounds') { if (info.target === 'bounds') {
// If we just clicked the selecting bounds's background, clear the selection // If we just clicked the selecting bounds's background, clear the selection
this.deselectAll() this.deselectAll()
@ -1412,41 +1461,41 @@ export class TLDrawState implements TLCallbacks {
this.pointedId = undefined this.pointedId = undefined
} }
this.setStatus('idle') this.setStatus(TLDrawStatus.Idle)
this.pointedId = undefined this.pointedId = undefined
break break
} }
case 'pointingBoundsHandle': { case TLDrawStatus.PointingBoundsHandle: {
this.setStatus('idle') this.setStatus(TLDrawStatus.Idle)
this.pointedBoundsHandle = undefined this.pointedBoundsHandle = undefined
break break
} }
case 'pointingHandle': { case TLDrawStatus.PointingHandle: {
this.setStatus('idle') this.setStatus(TLDrawStatus.Idle)
this.pointedHandle = undefined this.pointedHandle = undefined
break break
} }
case 'translatingHandle': { case TLDrawStatus.TranslatingHandle: {
this.completeSession<Sessions.HandleSession>() this.completeSession<Sessions.HandleSession>()
this.pointedHandle = undefined this.pointedHandle = undefined
break break
} }
case 'brushing': { case TLDrawStatus.Brushing: {
this.completeSession<Sessions.BrushSession>() this.completeSession<Sessions.BrushSession>()
brushUpdater.clear() brushUpdater.clear()
break break
} }
case 'translating': { case TLDrawStatus.Translating: {
this.completeSession(this.getPagePoint(info.point)) this.completeSession<Sessions.TranslateSession>()
this.pointedId = undefined this.pointedId = undefined
break break
} }
case 'transforming': { case TLDrawStatus.Transforming: {
this.completeSession(this.getPagePoint(info.point)) this.completeSession<Sessions.TransformSession>()
this.pointedBoundsHandle = undefined this.pointedBoundsHandle = undefined
break break
} }
case 'creating': { case TLDrawStatus.Creating: {
this.completeSession(this.getPagePoint(info.point)) this.completeSession(this.getPagePoint(info.point))
this.pointedHandle = undefined this.pointedHandle = undefined
} }
@ -1485,7 +1534,6 @@ export class TLDrawState implements TLCallbacks {
switch (this.appState.activeTool) { switch (this.appState.activeTool) {
case 'text': { case 'text': {
// Create a text shape // Create a text shape
this.setStatus('creating')
this.createActiveToolShape(info.point) this.createActiveToolShape(info.point)
break break
} }
@ -1530,7 +1578,7 @@ export class TLDrawState implements TLCallbacks {
this.setSelectedIds([info.target], info.shiftKey) this.setSelectedIds([info.target], info.shiftKey)
} }
this.setStatus('pointingBounds') this.setStatus(TLDrawStatus.PointingBounds)
break break
} }
} }
@ -1581,7 +1629,7 @@ export class TLDrawState implements TLCallbacks {
// Bounds (bounding box background) // Bounds (bounding box background)
onPointBounds: TLBoundsEventHandler = () => { onPointBounds: TLBoundsEventHandler = () => {
this.setStatus('pointingBounds') this.setStatus(TLDrawStatus.PointingBounds)
} }
onDoubleClickBounds: TLBoundsEventHandler = () => { onDoubleClickBounds: TLBoundsEventHandler = () => {
@ -1606,11 +1654,11 @@ export class TLDrawState implements TLCallbacks {
onReleaseBounds: TLBoundsEventHandler = (info) => { onReleaseBounds: TLBoundsEventHandler = (info) => {
switch (this.status.current) { switch (this.status.current) {
case 'translating': { case TLDrawStatus.Translating: {
this.completeSession(this.getPagePoint(info.point)) this.completeSession(this.getPagePoint(info.point))
break break
} }
case 'brushing': { case TLDrawStatus.Brushing: {
this.completeSession<Sessions.BrushSession>() this.completeSession<Sessions.BrushSession>()
brushUpdater.clear() brushUpdater.clear()
break break
@ -1621,7 +1669,7 @@ export class TLDrawState implements TLCallbacks {
// Bounds handles (corners, edges) // Bounds handles (corners, edges)
onPointBoundsHandle: TLBoundsHandleEventHandler = (info) => { onPointBoundsHandle: TLBoundsHandleEventHandler = (info) => {
this.pointedBoundsHandle = info.target this.pointedBoundsHandle = info.target
this.setStatus('pointingBoundsHandle') this.setStatus(TLDrawStatus.PointingBoundsHandle)
} }
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => { onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => {
@ -1651,7 +1699,7 @@ export class TLDrawState implements TLCallbacks {
// Handles (ie the handles of a selected arrow) // Handles (ie the handles of a selected arrow)
onPointHandle: TLPointerEventHandler = (info) => { onPointHandle: TLPointerEventHandler = (info) => {
this.pointedHandle = info.target this.pointedHandle = info.target
this.setStatus('pointingHandle') this.setStatus(TLDrawStatus.PointingHandle)
} }
onDoubleClickHandle: TLPointerEventHandler = (info) => { onDoubleClickHandle: TLPointerEventHandler = (info) => {
@ -1766,4 +1814,8 @@ export class TLDrawState implements TLCallbacks {
get appState() { get appState() {
return this.data.appState return this.data.appState
} }
get status() {
return this.appState.status
}
} }

View file

@ -35,6 +35,7 @@ export interface Data {
isToolLocked: boolean isToolLocked: boolean
isStyleOpen: boolean isStyleOpen: boolean
isEmptyCanvas: boolean isEmptyCanvas: boolean
status: { current: TLDrawStatus; previous: TLDrawStatus }
} }
} }
export interface PagePartial { export interface PagePartial {
@ -63,25 +64,27 @@ export interface History {
export interface Session { export interface Session {
id: string id: string
status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => Partial<Data> start: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
update: (data: Readonly<Data>, ...args: any[]) => Partial<Data> update: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
complete: (data: Readonly<Data>, ...args: any[]) => Partial<Data> | Command complete: (data: Readonly<Data>, ...args: any[]) => Partial<Data> | Command
cancel: (data: Readonly<Data>, ...args: any[]) => Partial<Data> cancel: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
} }
export type TLDrawStatus = export enum TLDrawStatus {
| 'idle' Idle = 'idle',
| 'pointingHandle' PointingHandle = 'pointingHandle',
| 'pointingBounds' PointingBounds = 'pointingBounds',
| 'pointingBoundsHandle' PointingBoundsHandle = 'pointingBoundsHandle',
| 'translatingHandle' TranslatingHandle = 'translatingHandle',
| 'translating' Translating = 'translating',
| 'transforming' Transforming = 'transforming',
| 'rotating' Rotating = 'rotating',
| 'pinching' Pinching = 'pinching',
| 'brushing' Brushing = 'brushing',
| 'creating' Creating = 'creating',
| 'editing-text' EditingText = 'editing-text',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never export type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never