Add shift+click to draw straight lines between points (#478)
* Add shift+click to draw straight lines between points * Add points to previous shape * undo/redo fixes * Fix bug with non-draw shapes * Update drawHelpers.ts
This commit is contained in:
parent
97ba1505ab
commit
d00d443492
5 changed files with 172 additions and 101 deletions
|
@ -1356,12 +1356,8 @@ left past the initial left edge) then swap points on that axis.
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, point, i, arr) => {
|
(acc, point, i, arr) => {
|
||||||
if (i === max) {
|
if (i === max) {
|
||||||
if (closed) {
|
if (closed) acc.push('Z')
|
||||||
acc.push('Z')
|
} else acc.push(point, Vec.med(point, arr[i + 1]))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc.push(point, Vec.med(point, arr[i + 1]))
|
|
||||||
}
|
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
['M', points[0], 'Q']
|
['M', points[0], 'Q']
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLPerformanceMode, Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { SessionType, TDStatus, TldrawPatch, TldrawCommand, DrawShape } from '~types'
|
import { SessionType, TDStatus, TldrawPatch, TldrawCommand, DrawShape } from '~types'
|
||||||
import type { TldrawApp } from '../../internal'
|
import type { TldrawApp } from '../../internal'
|
||||||
|
@ -10,27 +10,78 @@ export class DrawSession extends BaseSession {
|
||||||
status = TDStatus.Creating
|
status = TDStatus.Creating
|
||||||
topLeft: number[]
|
topLeft: number[]
|
||||||
points: number[][]
|
points: number[][]
|
||||||
|
initialShape: DrawShape
|
||||||
lastAdjustedPoint: number[]
|
lastAdjustedPoint: number[]
|
||||||
shiftedPoints: number[][] = []
|
shiftedPoints: number[][] = []
|
||||||
shapeId: string
|
shapeId: string
|
||||||
isLocked?: boolean
|
isLocked?: boolean
|
||||||
|
isExtending: boolean
|
||||||
lockedDirection?: 'horizontal' | 'vertical'
|
lockedDirection?: 'horizontal' | 'vertical'
|
||||||
|
|
||||||
constructor(app: TldrawApp, id: string) {
|
constructor(app: TldrawApp, id: string) {
|
||||||
super(app)
|
super(app)
|
||||||
const { originPoint } = this.app
|
const { originPoint } = this.app
|
||||||
this.topLeft = [...originPoint]
|
|
||||||
this.shapeId = id
|
this.shapeId = id
|
||||||
|
this.initialShape = this.app.getShape<DrawShape>(id)
|
||||||
|
this.topLeft = [...this.initialShape.point]
|
||||||
|
const currentPoint = [0, 0, originPoint[2] ?? 0.5]
|
||||||
|
const delta = Vec.sub(originPoint, this.topLeft)
|
||||||
|
const initialPoints = this.initialShape.points.map((pt) => Vec.sub(pt, delta).concat(pt[2]))
|
||||||
|
this.isExtending = initialPoints.length > 0
|
||||||
|
const newPoints: number[][] = []
|
||||||
|
if (this.isExtending) {
|
||||||
|
const prevPoint = initialPoints[initialPoints.length - 1]
|
||||||
|
newPoints.push(prevPoint, prevPoint)
|
||||||
|
// Continuing with shift
|
||||||
|
const len = Math.ceil(Vec.dist(prevPoint, currentPoint) / 16)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const t = i / (len - 1)
|
||||||
|
newPoints.push(Vec.lrp(prevPoint, currentPoint, t).concat(prevPoint[2]))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPoints.push(currentPoint)
|
||||||
|
}
|
||||||
// Add a first point but don't update the shape yet. We'll update
|
// 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
|
// when the draw session ends; if the user hasn't added additional
|
||||||
// points, this single point will be interpreted as a "dot" shape.
|
// points, this single point will be interpreted as a "dot" shape.
|
||||||
this.points = [[0, 0, originPoint[2] || 0.5]]
|
this.points = [...initialPoints, ...newPoints]
|
||||||
this.shiftedPoints = [...this.points]
|
this.shiftedPoints = this.points.map((pt) => Vec.add(pt, delta).concat(pt[2]))
|
||||||
this.lastAdjustedPoint = [0, 0]
|
this.lastAdjustedPoint = this.points[this.points.length - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (): TldrawPatch | undefined => void null
|
start = () => {
|
||||||
|
const currentPoint = this.app.originPoint
|
||||||
|
const newAdjustedPoint = [0, 0, currentPoint[2] ?? 0.5]
|
||||||
|
// Add the new adjusted point to the points array
|
||||||
|
this.points.push(newAdjustedPoint)
|
||||||
|
const topLeft = [
|
||||||
|
Math.min(this.topLeft[0], currentPoint[0]),
|
||||||
|
Math.min(this.topLeft[1], currentPoint[1]),
|
||||||
|
]
|
||||||
|
const delta = Vec.sub(topLeft, currentPoint)
|
||||||
|
this.topLeft = topLeft
|
||||||
|
this.shiftedPoints = this.points.map((pt) => Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
[this.app.currentPageId]: {
|
||||||
|
shapes: {
|
||||||
|
[this.shapeId]: {
|
||||||
|
point: this.topLeft,
|
||||||
|
points: this.shiftedPoints,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
[this.app.currentPageId]: {
|
||||||
|
selectedIds: [this.shapeId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update = (): TldrawPatch | undefined => {
|
update = (): TldrawPatch | undefined => {
|
||||||
const { shapeId } = this
|
const { shapeId } = this
|
||||||
|
@ -81,58 +132,16 @@ export class DrawSession extends BaseSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The new adjusted point
|
const change = this.addPoint(currentPoint)
|
||||||
const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
|
|
||||||
|
|
||||||
// Don't add duplicate points.
|
if (!change) return
|
||||||
if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return
|
|
||||||
|
|
||||||
// Add the new adjusted point to the points array
|
|
||||||
this.points.push(newAdjustedPoint)
|
|
||||||
|
|
||||||
// The new adjusted point is now the previous adjusted point.
|
|
||||||
this.lastAdjustedPoint = newAdjustedPoint
|
|
||||||
|
|
||||||
// Does the input point create a new top left?
|
|
||||||
const prevTopLeft = [...this.topLeft]
|
|
||||||
|
|
||||||
const topLeft = [
|
|
||||||
Math.min(this.topLeft[0], currentPoint[0]),
|
|
||||||
Math.min(this.topLeft[1], currentPoint[1]),
|
|
||||||
]
|
|
||||||
|
|
||||||
const delta = Vec.sub(topLeft, originPoint)
|
|
||||||
|
|
||||||
// Time to shift some points!
|
|
||||||
let points: number[][]
|
|
||||||
|
|
||||||
if (prevTopLeft[0] !== topLeft[0] || prevTopLeft[1] !== topLeft[1]) {
|
|
||||||
this.topLeft = topLeft
|
|
||||||
// If we have a new top left, then we need to iterate through
|
|
||||||
// the "unshifted" points array and shift them based on the
|
|
||||||
// offset between the new top left and the original top left.
|
|
||||||
|
|
||||||
points = this.points.map((pt) => {
|
|
||||||
return Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// If the new top left is the same as the previous top left,
|
|
||||||
// we don't need to shift anything: we just shift the new point
|
|
||||||
// and add it to the shifted points array.
|
|
||||||
points = [...this.shiftedPoints, Vec.sub(newAdjustedPoint, delta).concat(newAdjustedPoint[2])]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shiftedPoints = points
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[this.app.currentPageId]: {
|
[this.app.currentPageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[shapeId]: {
|
[shapeId]: change,
|
||||||
point: this.topLeft,
|
|
||||||
points,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -154,7 +163,7 @@ export class DrawSession extends BaseSession {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[shapeId]: undefined,
|
[shapeId]: this.isExtending ? this.initialShape : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -170,9 +179,7 @@ export class DrawSession extends BaseSession {
|
||||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||||
const { shapeId } = this
|
const { shapeId } = this
|
||||||
const pageId = this.app.currentPageId
|
const pageId = this.app.currentPageId
|
||||||
|
|
||||||
const shape = this.app.getShape<DrawShape>(shapeId)
|
const shape = this.app.getShape<DrawShape>(shapeId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'create_draw',
|
id: 'create_draw',
|
||||||
before: {
|
before: {
|
||||||
|
@ -180,7 +187,7 @@ export class DrawSession extends BaseSession {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[shapeId]: undefined,
|
[shapeId]: this.isExtending ? this.initialShape : undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -214,4 +221,52 @@ export class DrawSession extends BaseSession {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addPoint = (currentPoint: number[]) => {
|
||||||
|
const { originPoint } = this.app
|
||||||
|
// The new adjusted point
|
||||||
|
const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
|
||||||
|
|
||||||
|
// Don't add duplicate points.
|
||||||
|
if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return
|
||||||
|
|
||||||
|
// Add the new adjusted point to the points array
|
||||||
|
this.points.push(newAdjustedPoint)
|
||||||
|
|
||||||
|
// The new adjusted point is now the previous adjusted point.
|
||||||
|
this.lastAdjustedPoint = newAdjustedPoint
|
||||||
|
|
||||||
|
// Does the input point create a new top left?
|
||||||
|
const prevTopLeft = [...this.topLeft]
|
||||||
|
|
||||||
|
const topLeft = [
|
||||||
|
Math.min(this.topLeft[0], currentPoint[0]),
|
||||||
|
Math.min(this.topLeft[1], currentPoint[1]),
|
||||||
|
]
|
||||||
|
|
||||||
|
const delta = Vec.sub(topLeft, originPoint)
|
||||||
|
|
||||||
|
// Time to shift some points!
|
||||||
|
let points: number[][]
|
||||||
|
|
||||||
|
if (prevTopLeft[0] !== topLeft[0] || prevTopLeft[1] !== topLeft[1]) {
|
||||||
|
this.topLeft = topLeft
|
||||||
|
// If we have a new top left, then we need to iterate through
|
||||||
|
// the "unshifted" points array and shift them based on the
|
||||||
|
// offset between the new top left and the original top left.
|
||||||
|
points = this.points.map((pt) => Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2]))
|
||||||
|
} else {
|
||||||
|
// If the new top left is the same as the previous top left,
|
||||||
|
// we don't need to shift anything: we just shift the new point
|
||||||
|
// and add it to the shifted points array.
|
||||||
|
points = [...this.shiftedPoints, Vec.sub(newAdjustedPoint, delta).concat(newAdjustedPoint[2])]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shiftedPoints = points
|
||||||
|
|
||||||
|
return {
|
||||||
|
point: this.topLeft,
|
||||||
|
points,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand'
|
import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand'
|
||||||
import type { DrawShape } from '~types'
|
import type { DrawShape } from '~types'
|
||||||
import { getShapeStyle } from '../shared/shape-styles'
|
import { getShapeStyle } from '../shared/shape-styles'
|
||||||
|
@ -45,15 +46,9 @@ export function getDrawStrokePoints(shape: DrawShape, options: StrokeOptions) {
|
||||||
*/
|
*/
|
||||||
export function getDrawStrokePathTDSnapshot(shape: DrawShape) {
|
export function getDrawStrokePathTDSnapshot(shape: DrawShape) {
|
||||||
if (shape.points.length < 2) return ''
|
if (shape.points.length < 2) return ''
|
||||||
|
|
||||||
const options = getFreehandOptions(shape)
|
const options = getFreehandOptions(shape)
|
||||||
|
|
||||||
const strokePoints = getDrawStrokePoints(shape, options)
|
const strokePoints = getDrawStrokePoints(shape, options)
|
||||||
|
const path = Utils.getSvgPathFromStroke(getStrokeOutlinePoints(strokePoints, options))
|
||||||
const stroke = getStrokeOutlinePoints(strokePoints, options)
|
|
||||||
|
|
||||||
const path = Utils.getSvgPathFromStroke(stroke)
|
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,14 +57,11 @@ export function getDrawStrokePathTDSnapshot(shape: DrawShape) {
|
||||||
*/
|
*/
|
||||||
export function getSolidStrokePathTDSnapshot(shape: DrawShape) {
|
export function getSolidStrokePathTDSnapshot(shape: DrawShape) {
|
||||||
const { points } = shape
|
const { points } = shape
|
||||||
|
|
||||||
if (points.length < 2) return 'M 0 0 L 0 0'
|
if (points.length < 2) return 'M 0 0 L 0 0'
|
||||||
|
|
||||||
const options = getFreehandOptions(shape)
|
const options = getFreehandOptions(shape)
|
||||||
|
|
||||||
const strokePoints = getDrawStrokePoints(shape, options).map((pt) => pt.point.slice(0, 2))
|
const strokePoints = getDrawStrokePoints(shape, options).map((pt) => pt.point.slice(0, 2))
|
||||||
|
const last = points[points.length - 1].slice(0, 2)
|
||||||
|
if (!Vec.isEqual(strokePoints[0], last)) strokePoints.push(last)
|
||||||
const path = Utils.getSvgPathFromStroke(strokePoints, false)
|
const path = Utils.getSvgPathFromStroke(strokePoints, false)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +1,80 @@
|
||||||
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||||
import { Draw } from '~state/shapes'
|
import { Draw } from '~state/shapes'
|
||||||
import { SessionType, TDShapeType } from '~types'
|
import { SessionType, TDShapeType } from '~types'
|
||||||
import { BaseTool, Status } from '../BaseTool'
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
Extending = 'extending',
|
||||||
|
Pinching = 'pinching',
|
||||||
|
}
|
||||||
|
|
||||||
export class DrawTool extends BaseTool {
|
export class DrawTool extends BaseTool {
|
||||||
type = TDShapeType.Draw as const
|
type = TDShapeType.Draw as const
|
||||||
|
|
||||||
|
private lastShapeId?: string
|
||||||
|
|
||||||
|
onEnter = () => {
|
||||||
|
this.lastShapeId = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
switch (this.status) {
|
||||||
|
case Status.Idle: {
|
||||||
|
this.app.selectTool('select')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.cancelSession()
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------- Event Handlers ----------------- */
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
onPointerDown: TLPointerEventHandler = (info) => {
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
if (this.status !== Status.Idle) return
|
if (this.status !== Status.Idle) return
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentPoint,
|
currentPoint,
|
||||||
appState: { currentPageId, currentStyle },
|
appState: { currentPageId, currentStyle },
|
||||||
} = this.app
|
} = this.app
|
||||||
|
if (info.shiftKey && this.lastShapeId) {
|
||||||
|
// Extend the previous shape
|
||||||
|
this.app.startSession(SessionType.Draw, this.lastShapeId)
|
||||||
|
this.setStatus(Status.Extending)
|
||||||
|
} else {
|
||||||
|
// Create a new shape
|
||||||
const childIndex = this.getNextChildIndex()
|
const childIndex = this.getNextChildIndex()
|
||||||
|
|
||||||
const id = Utils.uniqueId()
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
const newShape = Draw.create({
|
const newShape = Draw.create({
|
||||||
id,
|
id,
|
||||||
parentId: currentPageId,
|
parentId: currentPageId,
|
||||||
childIndex,
|
childIndex,
|
||||||
point: [...currentPoint, info.pressure || 0.5],
|
point: currentPoint,
|
||||||
style: { ...currentStyle },
|
style: { ...currentStyle },
|
||||||
})
|
})
|
||||||
|
this.lastShapeId = id
|
||||||
this.app.patchCreate([newShape])
|
this.app.patchCreate([newShape])
|
||||||
|
|
||||||
this.app.startSession(SessionType.Draw, id)
|
this.app.startSession(SessionType.Draw, id)
|
||||||
|
|
||||||
this.setStatus(Status.Creating)
|
this.setStatus(Status.Creating)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPointerMove: TLPointerEventHandler = () => {
|
onPointerMove: TLPointerEventHandler = () => {
|
||||||
if (this.status === Status.Creating) {
|
switch (this.status) {
|
||||||
|
case Status.Extending:
|
||||||
|
case Status.Creating: {
|
||||||
this.app.updateSession()
|
this.app.updateSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPointerUp: TLPointerEventHandler = () => {
|
onPointerUp: TLPointerEventHandler = () => {
|
||||||
if (this.status === Status.Creating) {
|
|
||||||
this.app.completeSession()
|
this.app.completeSession()
|
||||||
}
|
|
||||||
|
|
||||||
this.setStatus(Status.Idle)
|
this.setStatus(Status.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -486,7 +486,6 @@ export class Vec {
|
||||||
* @param A The first point.
|
* @param A The first point.
|
||||||
* @param B The second point.
|
* @param B The second point.
|
||||||
* @param steps The number of points to return.
|
* @param steps The number of points to return.
|
||||||
* @param ease An easing function to apply to the simulated pressure.
|
|
||||||
*/
|
*/
|
||||||
static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
|
static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
|
||||||
return Array.from(Array(steps)).map((_, i) => {
|
return Array.from(Array(steps)).map((_, i) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue