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:
Steve Ruiz 2022-01-03 09:49:34 +00:00 committed by GitHub
parent 97ba1505ab
commit d00d443492
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 101 deletions

View file

@ -1356,12 +1356,8 @@ left past the initial left edge) then swap points on that axis.
.reduce(
(acc, point, i, arr) => {
if (i === max) {
if (closed) {
acc.push('Z')
}
} else {
acc.push(point, Vec.med(point, arr[i + 1]))
}
if (closed) acc.push('Z')
} else acc.push(point, Vec.med(point, arr[i + 1]))
return acc
},
['M', points[0], 'Q']

View file

@ -1,4 +1,4 @@
import { TLPerformanceMode, Utils } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { SessionType, TDStatus, TldrawPatch, TldrawCommand, DrawShape } from '~types'
import type { TldrawApp } from '../../internal'
@ -10,27 +10,78 @@ export class DrawSession extends BaseSession {
status = TDStatus.Creating
topLeft: number[]
points: number[][]
initialShape: DrawShape
lastAdjustedPoint: number[]
shiftedPoints: number[][] = []
shapeId: string
isLocked?: boolean
isExtending: boolean
lockedDirection?: 'horizontal' | 'vertical'
constructor(app: TldrawApp, id: string) {
super(app)
const { originPoint } = this.app
this.topLeft = [...originPoint]
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
// 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, originPoint[2] || 0.5]]
this.shiftedPoints = [...this.points]
this.lastAdjustedPoint = [0, 0]
this.points = [...initialPoints, ...newPoints]
this.shiftedPoints = this.points.map((pt) => Vec.add(pt, delta).concat(pt[2]))
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 => {
const { shapeId } = this
@ -81,58 +132,16 @@ export class DrawSession extends BaseSession {
}
}
// The new adjusted point
const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
const change = this.addPoint(currentPoint)
// 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) => {
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
if (!change) return
return {
document: {
pages: {
[this.app.currentPageId]: {
shapes: {
[shapeId]: {
point: this.topLeft,
points,
},
[shapeId]: change,
},
},
},
@ -154,7 +163,7 @@ export class DrawSession extends BaseSession {
pages: {
[pageId]: {
shapes: {
[shapeId]: undefined,
[shapeId]: this.isExtending ? this.initialShape : undefined,
},
},
},
@ -170,9 +179,7 @@ export class DrawSession extends BaseSession {
complete = (): TldrawPatch | TldrawCommand | undefined => {
const { shapeId } = this
const pageId = this.app.currentPageId
const shape = this.app.getShape<DrawShape>(shapeId)
return {
id: 'create_draw',
before: {
@ -180,7 +187,7 @@ export class DrawSession extends BaseSession {
pages: {
[pageId]: {
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,
}
}
}

View file

@ -1,4 +1,5 @@
import { Utils } from '@tldraw/core'
import Vec from '@tldraw/vec'
import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand'
import type { DrawShape } from '~types'
import { getShapeStyle } from '../shared/shape-styles'
@ -45,15 +46,9 @@ export function getDrawStrokePoints(shape: DrawShape, options: StrokeOptions) {
*/
export function getDrawStrokePathTDSnapshot(shape: DrawShape) {
if (shape.points.length < 2) return ''
const options = getFreehandOptions(shape)
const strokePoints = getDrawStrokePoints(shape, options)
const stroke = getStrokeOutlinePoints(strokePoints, options)
const path = Utils.getSvgPathFromStroke(stroke)
const path = Utils.getSvgPathFromStroke(getStrokeOutlinePoints(strokePoints, options))
return path
}
@ -62,14 +57,11 @@ export function getDrawStrokePathTDSnapshot(shape: DrawShape) {
*/
export function getSolidStrokePathTDSnapshot(shape: DrawShape) {
const { points } = shape
if (points.length < 2) return 'M 0 0 L 0 0'
const options = getFreehandOptions(shape)
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)
return path
}

View file

@ -1,51 +1,80 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core'
import { Draw } from '~state/shapes'
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 {
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 ----------------- */
onPointerDown: TLPointerEventHandler = (info) => {
if (this.status !== Status.Idle) return
const {
currentPoint,
appState: { currentPageId, currentStyle },
} = 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 id = Utils.uniqueId()
const newShape = Draw.create({
id,
parentId: currentPageId,
childIndex,
point: [...currentPoint, info.pressure || 0.5],
point: currentPoint,
style: { ...currentStyle },
})
this.lastShapeId = id
this.app.patchCreate([newShape])
this.app.startSession(SessionType.Draw, id)
this.setStatus(Status.Creating)
}
}
onPointerMove: TLPointerEventHandler = () => {
if (this.status === Status.Creating) {
switch (this.status) {
case Status.Extending:
case Status.Creating: {
this.app.updateSession()
}
}
}
onPointerUp: TLPointerEventHandler = () => {
if (this.status === Status.Creating) {
this.app.completeSession()
}
this.setStatus(Status.Idle)
}
}

View file

@ -486,7 +486,6 @@ export class Vec {
* @param A The first point.
* @param B The second point.
* @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[][] => {
return Array.from(Array(steps)).map((_, i) => {