Adds right click to select, fixes spacing in context menu, improves draw session

This commit is contained in:
Steve Ruiz 2021-08-18 08:19:13 +01:00
parent 74f600aac2
commit 429a5e6171
9 changed files with 134 additions and 91 deletions

View file

@ -8,9 +8,15 @@ export function useShapeEvents(id: string, disable = false) {
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (disable) return
if (e.button === 2) {
callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
return
}
if (e.button !== 0) return
const info = inputs.pointerDown(e, id)
e.stopPropagation()

View file

@ -3,7 +3,7 @@ import * as React from 'react'
import { openDB, DBSchema } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw'
const VERSION = 4
const VERSION = 5
interface TLDatabase extends DBSchema {
documents: {
@ -58,9 +58,9 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
React.useEffect(() => {
async function handleLoad() {
const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db, oldVersion, newVersion) {
upgrade(db, _oldVersion, newVersion) {
if (newVersion) {
if (newVersion > oldVersion) {
if (db.objectStoreNames.contains('documents')) {
db.deleteObjectStore('documents')
}
db.createObjectStore('documents')

View file

@ -160,19 +160,19 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
<ContextMenuSubMenu label="Move">
<ContextMenuButton onSelect={handleMoveToFront}>
<span>To Front</span>
<Kbd variant="menu"># ]</Kbd>
<Kbd variant="menu">#]</Kbd>
</ContextMenuButton>
<ContextMenuButton onSelect={handleMoveForward}>
<span>Forward</span>
<Kbd variant="menu"># ]</Kbd>
<Kbd variant="menu">#]</Kbd>
</ContextMenuButton>
<ContextMenuButton onSelect={handleMoveBackward}>
<span>Backward</span>
<Kbd variant="menu"># [</Kbd>
<Kbd variant="menu">#[</Kbd>
</ContextMenuButton>
<ContextMenuButton onSelect={handleMoveToBack}>
<span>To Back</span>
<Kbd variant="menu"># [</Kbd>
<Kbd variant="menu">#[</Kbd>
</ContextMenuButton>
</ContextMenuSubMenu>
{hasTwoOrMore && (
@ -182,12 +182,12 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
{isDebugMode && (
<ContextMenuButton onSelect={handleCopyAsJson}>
<span>Copy Data</span>
<Kbd variant="menu"># C</Kbd>
<Kbd variant="menu">#C</Kbd>
</ContextMenuButton>
)}
<ContextMenuButton onSelect={handleCopyAsSvg}>
<span>Copy to SVG</span>
<Kbd variant="menu"># C</Kbd>
<Kbd variant="menu">#C</Kbd>
</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton onSelect={handleDelete}>
@ -199,11 +199,11 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
<>
<ContextMenuButton onSelect={handleUndo}>
<span>Undo</span>
<Kbd variant="menu"># Z</Kbd>
<Kbd variant="menu">#Z</Kbd>
</ContextMenuButton>
<ContextMenuButton onSelect={handleRedo}>
<span>Redo</span>
<Kbd variant="menu"># Z</Kbd>
<Kbd variant="menu">#Z</Kbd>
</ContextMenuButton>
</>
)}

View file

@ -167,9 +167,11 @@ const Layout = styled('main', {
boxSizing: 'border-box',
outline: 'none',
pointerEvents: 'none',
'& > *': {
pointerEvents: 'all',
},
'& .tl-container': {
position: 'absolute',
top: 0,

View file

@ -21,7 +21,7 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
parentId: 'page',
childIndex: 1,
point: [0, 0],
points: [[0, 0, 0.5]],
points: [],
rotation: 0,
style: defaultStyle,
}
@ -30,12 +30,12 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
return next.points !== prev.points || next.style !== prev.style
}
render(shape: DrawShape, { isDarkMode }: TLRenderInfo): JSX.Element {
render(shape: DrawShape, { isDarkMode, isEditing }: TLRenderInfo): JSX.Element {
const { points, style } = shape
const styles = getShapeStyle(style, isDarkMode)
const strokeWidth = +styles.strokeWidth
const strokeWidth = styles.strokeWidth
const shouldFill =
style.isFilled &&
@ -43,8 +43,7 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
// For very short lines, draw a point instead of a line
if (points.length > 0 && points.length < 3) {
if (points.length === 1) {
const sw = strokeWidth * 0.618
return (
@ -65,9 +64,9 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
getFillPath(shape)
)
const drawPathData = Utils.getFromCache(this.drawPathCache, points, () =>
getDrawStrokePath(shape)
)
const drawPathData = isEditing
? getDrawStrokePath(shape, true)
: Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false))
return (
<>
@ -278,15 +277,15 @@ function getFillPath(shape: DrawShape) {
return Utils.getSvgPathFromStroke(
getStrokePoints(shape.points, {
size: 1 + +styles.strokeWidth * 2,
size: 1 + styles.strokeWidth * 2,
thinning: 0.85,
end: { taper: +styles.strokeWidth * 20 },
start: { taper: +styles.strokeWidth * 20 },
end: { taper: +styles.strokeWidth * 10 },
start: { taper: +styles.strokeWidth * 10 },
}).map((pt) => pt.point)
)
}
function getDrawStrokePath(shape: DrawShape) {
function getDrawStrokePath(shape: DrawShape, isEditing: boolean) {
const styles = getShapeStyle(shape.style)
if (shape.points.length < 2) {
@ -295,15 +294,20 @@ function getDrawStrokePath(shape: DrawShape) {
const options = shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
const stroke = getStroke(shape.points, {
size: 1 + +styles.strokeWidth * 2,
const stroke = getStroke(shape.points.slice(2), {
size: 1 + styles.strokeWidth * 2,
thinning: 0.85,
end: { taper: +styles.strokeWidth * 10 },
start: { taper: +styles.strokeWidth * 10 },
end: { taper: +styles.strokeWidth * 50 },
start: { taper: +styles.strokeWidth * 50 },
...options,
last: !isEditing,
})
return Utils.getSvgPathFromStroke(stroke)
const path = Utils.getSvgPathFromStroke(stroke)
// console.log(path)
return path
}
function getSolidStrokePath(shape: DrawShape) {

View file

@ -23,7 +23,7 @@ export class DrawSession implements Session {
// Add a first point but don't update the shape yet. We'll update
// when the draw session ends; if the user hasn't added additional
// points, this single point will be interpreted as a "dot" shape.
this.points = [[0, 0, 0.5]]
this.points = []
}
start = () => void null
@ -31,6 +31,11 @@ export class DrawSession implements Session {
update = (data: Data, point: number[], pressure: number, isLocked = false) => {
const { snapshot } = this
// Roundabout way of preventing the "dot" from showing while drawing
if (this.points.length === 0) {
this.points.push([0, 0, pressure])
}
// Drawing while holding shift will "lock" the pen to either the
// x or y axis, depending on which direction has the greater
// delta. Pressing shift will also add more points to "return"
@ -78,16 +83,12 @@ export class DrawSession implements Session {
const newPoint = Vec.round([...Vec.sub(this.previous, this.origin), pressure])
if (Vec.isEqual(this.last, newPoint)) return data
if (Vec.isEqual(this.last, newPoint)) return
this.last = newPoint
this.points.push(newPoint)
// We draw a dot when the number of points is 1 or 2, so this guard
// prevents a "flash" of a dot when a user begins drawing a line.
if (this.points.length <= 2) return data
return {
document: {
pages: {
@ -133,6 +134,9 @@ export class DrawSession implements Session {
complete = (data: Data) => {
const { snapshot } = this
const pageId = data.appState.currentPageId
this.points.push(this.last)
return {
id: 'create_draw',
before: {
@ -158,7 +162,7 @@ export class DrawSession implements Session {
shapes: {
[snapshot.id]: TLDR.onSessionComplete(
data,
TLDR.getShape(data, snapshot.id, pageId),
{ ...TLDR.getShape(data, snapshot.id, pageId), points: [...this.points] },
pageId
),
},

View file

@ -220,11 +220,11 @@ export class TLDrawState implements TLCallbacks {
}
if (nextPageState.bindingId && !page.bindings[nextPageState.bindingId]) {
console.warn('Could not find the binding shape!', pageId)
console.warn('Could not find the binding binding!', pageId)
delete nextPageState.bindingId
}
if (nextPageState.editingId && !page.bindings[nextPageState.editingId]) {
if (nextPageState.editingId && !page.shapes[nextPageState.editingId]) {
console.warn('Could not find the editing shape!')
delete nextPageState.editingId
}
@ -702,7 +702,9 @@ export class TLDrawState implements TLCallbacks {
updateSession<T extends Session>(...args: ParametersExceptFirst<T['update']>) {
const { session } = this
if (!session) return this
this.produce(session.update(this.data, ...args), `session:update:${session.id}`)
const patch = session.update(this.data, ...args)
if (!patch) return
this.produce(patch, `session:update:${session.id}`)
return this
}
@ -777,6 +779,13 @@ export class TLDrawState implements TLCallbacks {
previous: this.appState.status.previous,
},
},
document: {
pageStates: {
[this.currentPageId]: {
editingId: undefined,
},
},
},
},
`session:complete:${session.id}`
)
@ -805,16 +814,27 @@ export class TLDrawState implements TLCallbacks {
},
},
}
result.after = {
...result.after,
document: {
...result.after.document,
pageStates: {
...result.after.document?.pageStates,
[this.currentPageId]: {
...(result.after.document?.pageStates || {})[this.currentPageId],
editingId: undefined,
},
},
},
}
}
result.after = {
...result.after,
appState: {
...result.after.appState,
status: {
current: TLDrawStatus.Idle,
previous: this.appState.status.previous,
},
result.after.appState = {
...result.after.appState,
status: {
current: TLDrawStatus.Idle,
previous: this.appState.status.previous,
},
}
@ -832,6 +852,13 @@ export class TLDrawState implements TLCallbacks {
previous: this.appState.status.previous,
},
},
document: {
pageStates: {
[this.currentPageId]: {
editingId: undefined,
},
},
},
},
`session:complete:${session.id}`
)
@ -971,105 +998,105 @@ export class TLDrawState implements TLCallbacks {
/* ----------------- Shape Functions ---------------- */
style = (style: Partial<ShapeStyles>, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.style(data, idsToMutate, style))
return this
}
align = (type: AlignType, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.align(data, idsToMutate, type))
return this
}
distribute = (type: DistributeType, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.distribute(data, idsToMutate, type))
return this
}
stretch = (type: StretchType, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.stretch(data, idsToMutate, type))
return this
}
flipHorizontal = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.flip(data, idsToMutate, FlipType.Horizontal))
return this
}
flipVertical = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.flip(data, idsToMutate, FlipType.Vertical))
return this
}
moveToBack = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.move(data, idsToMutate, MoveType.ToBack))
return this
}
moveBackward = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.move(data, idsToMutate, MoveType.Backward))
return this
}
moveForward = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.move(data, idsToMutate, MoveType.Forward))
return this
}
moveToFront = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.move(data, idsToMutate, MoveType.ToFront))
return this
}
nudge = (delta: number[], isMajor = false, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.translate(data, idsToMutate, Vec.mul(delta, isMajor ? 10 : 1)))
return this
}
duplicate = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.duplicate(data, idsToMutate))
return this
}
toggleHidden = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.toggle(data, idsToMutate, 'isHidden'))
return this
}
toggleLocked = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.toggle(data, idsToMutate, 'isLocked'))
return this
}
toggleAspectRatioLocked = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.toggle(data, idsToMutate, 'isAspectRatioLocked'))
return this
@ -1077,7 +1104,7 @@ export class TLDrawState implements TLCallbacks {
toggleDecoration = (handleId: string, ids?: string[]) => {
if (handleId === 'start' || handleId === 'end') {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.toggleDecoration(data, idsToMutate, handleId))
}
@ -1090,7 +1117,7 @@ export class TLDrawState implements TLCallbacks {
}
rotate = (delta = Math.PI * -0.5, ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
this.do(Commands.rotate(data, idsToMutate, delta))
return this
@ -1098,20 +1125,20 @@ export class TLDrawState implements TLCallbacks {
group = () => {
// TODO
// const data = this.store.getState()
// const data = this.data
// const idsToMutate = ids ? ids : this.selectedIds
// this.do(Commands.toggle(data, idsToMutate, 'isAspectRatioLocked'))
return this
}
create = (...shapes: TLDrawShape[]) => {
const data = this.store.getState()
const data = this.data
this.do(Commands.create(data, shapes))
return this
}
delete = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToMutate = ids ? ids : this.selectedIds
if (idsToMutate.length === 0) return this
@ -1186,7 +1213,7 @@ export class TLDrawState implements TLCallbacks {
}
copy = (ids?: string[]) => {
const data = this.store.getState()
const data = this.data
const idsToCopy = ids ? ids : this.selectedIds
this.clipboard = idsToCopy.map((id) => {
@ -1295,7 +1322,7 @@ export class TLDrawState implements TLCallbacks {
}
/* -------------------- Sessions -------------------- */
startBrushSession = (point: number[]) => {
this.startSession(new Sessions.BrushSession(this.store.getState(), point))
this.startSession(new Sessions.BrushSession(this.data, point))
return this
}
@ -1305,7 +1332,7 @@ export class TLDrawState implements TLCallbacks {
}
startTranslateSession = (point: number[]) => {
this.startSession(new Sessions.TranslateSession(this.store.getState(), point))
this.startSession(new Sessions.TranslateSession(this.data, point))
return this
}
@ -1326,20 +1353,13 @@ export class TLDrawState implements TLCallbacks {
this.pointedBoundsHandle = handle
if (this.pointedBoundsHandle === 'rotate') {
this.startSession(new Sessions.RotateSession(this.store.getState(), point))
this.startSession(new Sessions.RotateSession(this.data, point))
} else if (this.selectedIds.length === 1) {
this.startSession(
new Sessions.TransformSingleSession(
this.store.getState(),
point,
this.pointedBoundsHandle,
commandId
)
new Sessions.TransformSingleSession(this.data, point, this.pointedBoundsHandle, commandId)
)
} else {
this.startSession(
new Sessions.TransformSession(this.store.getState(), point, this.pointedBoundsHandle)
)
this.startSession(new Sessions.TransformSession(this.data, point, this.pointedBoundsHandle))
}
return this
}
@ -1354,7 +1374,7 @@ export class TLDrawState implements TLCallbacks {
}
startTextSession = (id?: string) => {
this.startSession(new Sessions.TextSession(this.store.getState(), id))
this.startSession(new Sessions.TextSession(this.data, id))
return this
}
@ -1364,7 +1384,7 @@ export class TLDrawState implements TLCallbacks {
}
startDrawSession = (id: string, point: number[]) => {
this.startSession(new Sessions.DrawSession(this.store.getState(), id, point))
this.startSession(new Sessions.DrawSession(this.data, id, point))
return this
}
@ -1377,11 +1397,11 @@ export class TLDrawState implements TLCallbacks {
const selectedShape = this.page.shapes[this.selectedIds[0]]
if (selectedShape.type === TLDrawShapeType.Arrow) {
this.startSession<Sessions.ArrowSession>(
new Sessions.ArrowSession(this.store.getState(), handleId as 'start' | 'end', point)
new Sessions.ArrowSession(this.data, handleId as 'start' | 'end', point)
)
} else {
this.startSession<Sessions.HandleSession>(
new Sessions.HandleSession(this.store.getState(), handleId, point, commandId)
new Sessions.HandleSession(this.data, handleId, point, commandId)
)
}
return this
@ -1511,6 +1531,7 @@ export class TLDrawState implements TLCallbacks {
pageStates: {
[this.currentPageId]: {
selectedIds: [id],
editingId: id,
},
},
},
@ -1777,6 +1798,10 @@ export class TLDrawState implements TLCallbacks {
// Canvas (background)
onPointCanvas: TLCanvasEventHandler = (info) => {
if (this.appState.isStyleOpen) {
this.toggleStylePanel()
}
switch (this.status.current) {
case 'idle': {
switch (this.appState.activeTool) {
@ -1871,8 +1896,10 @@ export class TLDrawState implements TLCallbacks {
// TODO (drill into group)
}
onRightPointShape: TLPointerEventHandler = () => {
// TODO
onRightPointShape: TLPointerEventHandler = (info) => {
if (!this.selectedIds.includes(info.target)) {
this.select(info.target)
}
}
onDragShape: TLPointerEventHandler = () => {

View file

@ -75,10 +75,10 @@ export interface SelectHistory {
export interface Session {
id: string
status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | void
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | Command | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
}
export enum TLDrawStatus {

View file

@ -58,9 +58,9 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
React.useEffect(() => {
async function handleLoad() {
const db1 = await openDB<TLDatabase>('db1', VERSION, {
upgrade(db, oldVersion, newVersion) {
upgrade(db, _oldVersion, newVersion) {
if (newVersion) {
if (newVersion > oldVersion) {
if (db.objectStoreNames.contains('documents')) {
db.deleteObjectStore('documents')
}
db.createObjectStore('documents')