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(
|
||||
(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']
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue