[feature] add erase line (#708)
* Add erase line * Fix erasing small dots * bump perfect-freehand
This commit is contained in:
parent
a5e2b55294
commit
c126be5c50
15 changed files with 113 additions and 5 deletions
|
@ -32,7 +32,7 @@
|
||||||
"immer": "^9.0.12",
|
"immer": "^9.0.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nanoid": "^3.1.31",
|
"nanoid": "^3.1.31",
|
||||||
"perfect-freehand": "^1.0.16",
|
"perfect-freehand": "^1.1.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.9",
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"@tldraw/vec": "^1.7.0",
|
"@tldraw/vec": "^1.7.0",
|
||||||
"@use-gesture/react": "^10.2.14",
|
"@use-gesture/react": "^10.2.14",
|
||||||
"mobx-react-lite": "^3.2.3",
|
"mobx-react-lite": "^3.2.3",
|
||||||
|
"perfect-freehand": "^1.1.0",
|
||||||
"resize-observer-polyfill": "^1.5.1"
|
"resize-observer-polyfill": "^1.5.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -30,12 +30,14 @@ import { UsersIndicators } from '~components/UsersIndicators'
|
||||||
import { SnapLines } from '~components/SnapLines/SnapLines'
|
import { SnapLines } from '~components/SnapLines/SnapLines'
|
||||||
import { Grid } from '~components/Grid'
|
import { Grid } from '~components/Grid'
|
||||||
import { Overlay } from '~components/Overlay'
|
import { Overlay } from '~components/Overlay'
|
||||||
|
import { EraseLine } from '~components/EraseLine'
|
||||||
|
|
||||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
assets: TLAssets
|
assets: TLAssets
|
||||||
snapLines?: TLSnapLine[]
|
snapLines?: TLSnapLine[]
|
||||||
|
eraseLine?: number[][]
|
||||||
grid?: number
|
grid?: number
|
||||||
users?: TLUsers<T>
|
users?: TLUsers<T>
|
||||||
userId?: string
|
userId?: string
|
||||||
|
@ -64,6 +66,7 @@ export const Canvas = observer(function _Canvas<
|
||||||
pageState,
|
pageState,
|
||||||
assets,
|
assets,
|
||||||
snapLines,
|
snapLines,
|
||||||
|
eraseLine,
|
||||||
grid,
|
grid,
|
||||||
users,
|
users,
|
||||||
userId,
|
userId,
|
||||||
|
@ -134,6 +137,7 @@ export const Canvas = observer(function _Canvas<
|
||||||
{users && <Users userId={userId} users={users} />}
|
{users && <Users userId={userId} users={users} />}
|
||||||
</div>
|
</div>
|
||||||
<Overlay camera={pageState.camera}>
|
<Overlay camera={pageState.camera}>
|
||||||
|
{eraseLine && <EraseLine points={eraseLine} />}
|
||||||
{snapLines && <SnapLines snapLines={snapLines} />}
|
{snapLines && <SnapLines snapLines={snapLines} />}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
|
18
packages/core/src/components/EraseLine/EraseLine.tsx
Normal file
18
packages/core/src/components/EraseLine/EraseLine.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import getStroke from 'perfect-freehand'
|
||||||
|
import Utils from '~utils'
|
||||||
|
|
||||||
|
export interface UiEraseLintProps {
|
||||||
|
points: number[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UiEraseLineComponent = (props: UiEraseLintProps) => any | null
|
||||||
|
|
||||||
|
export const EraseLine = observer(function EraserLine({ points }: UiEraseLintProps) {
|
||||||
|
if (points.length === 0) return null
|
||||||
|
|
||||||
|
const d = Utils.getSvgPathFromStroke(getStroke(points, { size: 16, start: { taper: true } }))
|
||||||
|
|
||||||
|
return <path d={d} className="tl-erase-line" />
|
||||||
|
})
|
1
packages/core/src/components/EraseLine/index.ts
Normal file
1
packages/core/src/components/EraseLine/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './EraseLine'
|
|
@ -56,6 +56,10 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
|
||||||
* (optional) The current snap lines to render.
|
* (optional) The current snap lines to render.
|
||||||
*/
|
*/
|
||||||
snapLines?: TLSnapLine[]
|
snapLines?: TLSnapLine[]
|
||||||
|
/**
|
||||||
|
* (optional) The current erase line to render.
|
||||||
|
*/
|
||||||
|
eraseLine?: number[][]
|
||||||
/**
|
/**
|
||||||
* (optional) The current user's id, used to identify the user.
|
* (optional) The current user's id, used to identify the user.
|
||||||
*/
|
*/
|
||||||
|
@ -141,6 +145,7 @@ export const Renderer = observer(function _Renderer<
|
||||||
theme,
|
theme,
|
||||||
meta,
|
meta,
|
||||||
snapLines,
|
snapLines,
|
||||||
|
eraseLine,
|
||||||
grid,
|
grid,
|
||||||
containerRef,
|
containerRef,
|
||||||
performanceMode,
|
performanceMode,
|
||||||
|
@ -196,6 +201,7 @@ export const Renderer = observer(function _Renderer<
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
assets={assets}
|
assets={assets}
|
||||||
snapLines={snapLines}
|
snapLines={snapLines}
|
||||||
|
eraseLine={eraseLine}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
users={users}
|
users={users}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
|
|
|
@ -367,6 +367,13 @@ export const TLCSS = css`
|
||||||
.tl-grid-dot {
|
.tl-grid-dot {
|
||||||
fill: var(--tl-grid);
|
fill: var(--tl-grid);
|
||||||
}
|
}
|
||||||
|
.tl-erase-line {
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-linecap: round;
|
||||||
|
pointer-events: none;
|
||||||
|
fill: var(--tl-grid);
|
||||||
|
opacity: 0.32;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export function useTLTheme(theme?: Partial<TLTheme>, selector?: string) {
|
export function useTLTheme(theme?: Partial<TLTheme>, selector?: string) {
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
"@types/lz-string": "^1.3.34",
|
"@types/lz-string": "^1.3.34",
|
||||||
"idb-keyval": "^6.1.0",
|
"idb-keyval": "^6.1.0",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"perfect-freehand": "^1.0.16",
|
"perfect-freehand": "^1.1.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-hotkey-hook": "^1.0.2",
|
"react-hotkey-hook": "^1.0.2",
|
||||||
"react-hotkeys-hook": "^3.4.4",
|
"react-hotkeys-hook": "^3.4.4",
|
||||||
|
|
|
@ -431,6 +431,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
assets={assets}
|
assets={assets}
|
||||||
snapLines={appState.snapLines}
|
snapLines={appState.snapLines}
|
||||||
|
eraseLine={appState.eraseLine}
|
||||||
grid={GRID_SIZE}
|
grid={GRID_SIZE}
|
||||||
users={room?.users}
|
users={room?.users}
|
||||||
userId={room?.userId}
|
userId={room?.userId}
|
||||||
|
|
|
@ -198,7 +198,7 @@ export class StateManager<T extends Record<string, any>> {
|
||||||
* @param patch The patch to apply.
|
* @param patch The patch to apply.
|
||||||
* @param id (optional) An id for this patch.
|
* @param id (optional) An id for this patch.
|
||||||
*/
|
*/
|
||||||
protected patchState = (patch: Patch<T>, id?: string): this => {
|
patchState = (patch: Patch<T>, id?: string): this => {
|
||||||
this.applyPatch(patch, id)
|
this.applyPatch(patch, id)
|
||||||
if (this.onPatch) {
|
if (this.onPatch) {
|
||||||
this.onPatch(this._state, id)
|
this.onPatch(this._state, id)
|
||||||
|
|
|
@ -4103,6 +4103,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
isToolLocked: false,
|
isToolLocked: false,
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
isEmptyCanvas: false,
|
isEmptyCanvas: false,
|
||||||
|
eraseLine: [],
|
||||||
snapLines: [],
|
snapLines: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
disableAssets: false,
|
disableAssets: false,
|
||||||
|
|
|
@ -22,12 +22,56 @@ export class EraseSession extends BaseSession {
|
||||||
initialSelectedShapes: TDShape[]
|
initialSelectedShapes: TDShape[]
|
||||||
erasableShapes: Set<TDShape>
|
erasableShapes: Set<TDShape>
|
||||||
prevPoint: number[]
|
prevPoint: number[]
|
||||||
|
prevEraseShapesSize = 0
|
||||||
|
|
||||||
constructor(app: TldrawApp) {
|
constructor(app: TldrawApp) {
|
||||||
super(app)
|
super(app)
|
||||||
this.prevPoint = [...app.originPoint]
|
this.prevPoint = [...app.originPoint]
|
||||||
this.initialSelectedShapes = this.app.selectedIds.map((id) => this.app.getShape(id))
|
this.initialSelectedShapes = this.app.selectedIds.map((id) => this.app.getShape(id))
|
||||||
this.erasableShapes = new Set(this.app.shapes.filter((shape) => !shape.isLocked))
|
this.erasableShapes = new Set(this.app.shapes.filter((shape) => !shape.isLocked))
|
||||||
|
this.interval = this.loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
interval: any
|
||||||
|
timestamp1 = 0
|
||||||
|
timestamp2 = 0
|
||||||
|
prevErasePoint: number[] = []
|
||||||
|
|
||||||
|
loop = () => {
|
||||||
|
const now = Date.now()
|
||||||
|
const elapsed1 = now - this.timestamp1
|
||||||
|
const elapsed2 = now - this.timestamp2
|
||||||
|
const { eraseLine } = this.app.appState
|
||||||
|
|
||||||
|
let next = [...eraseLine]
|
||||||
|
let didUpdate = false
|
||||||
|
|
||||||
|
if (elapsed1 > 16 && this.prevErasePoint !== this.prevPoint) {
|
||||||
|
didUpdate = true
|
||||||
|
next = [...eraseLine, this.prevPoint]
|
||||||
|
this.prevErasePoint = this.prevPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed2 > 32) {
|
||||||
|
if (next.length > 1) {
|
||||||
|
didUpdate = true
|
||||||
|
next.splice(0, Math.ceil(next.length * 0.1))
|
||||||
|
this.timestamp2 = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
this.app.patchState(
|
||||||
|
{
|
||||||
|
appState: {
|
||||||
|
eraseLine: next,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'eraseline'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.interval = requestAnimationFrame(this.loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (): TldrawPatch | undefined => void null
|
start = (): TldrawPatch | undefined => void null
|
||||||
|
@ -100,6 +144,12 @@ export class EraseSession extends BaseSession {
|
||||||
|
|
||||||
this.prevPoint = newPoint
|
this.prevPoint = newPoint
|
||||||
|
|
||||||
|
if (erasedShapes.length === this.prevEraseShapesSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prevEraseShapesSize = erasedShapes.length
|
||||||
|
|
||||||
return {
|
return {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
|
@ -114,6 +164,8 @@ export class EraseSession extends BaseSession {
|
||||||
cancel = (): TldrawPatch | undefined => {
|
cancel = (): TldrawPatch | undefined => {
|
||||||
const { page } = this.app
|
const { page } = this.app
|
||||||
|
|
||||||
|
cancelAnimationFrame(this.interval)
|
||||||
|
|
||||||
this.erasedShapes.forEach((shape) => {
|
this.erasedShapes.forEach((shape) => {
|
||||||
if (!this.app.getShape(shape.id)) {
|
if (!this.app.getShape(shape.id)) {
|
||||||
this.erasedShapes.delete(shape)
|
this.erasedShapes.delete(shape)
|
||||||
|
@ -136,12 +188,17 @@ export class EraseSession extends BaseSession {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appState: {
|
||||||
|
eraseLine: [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||||
const { page } = this.app
|
const { page } = this.app
|
||||||
|
|
||||||
|
cancelAnimationFrame(this.interval)
|
||||||
|
|
||||||
this.erasedShapes.forEach((shape) => {
|
this.erasedShapes.forEach((shape) => {
|
||||||
if (!this.app.getShape(shape.id)) {
|
if (!this.app.getShape(shape.id)) {
|
||||||
this.erasedShapes.delete(shape)
|
this.erasedShapes.delete(shape)
|
||||||
|
@ -217,6 +274,9 @@ export class EraseSession extends BaseSession {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appState: {
|
||||||
|
eraseLine: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
after: {
|
after: {
|
||||||
document: {
|
document: {
|
||||||
|
@ -232,6 +292,9 @@ export class EraseSession extends BaseSession {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appState: {
|
||||||
|
eraseLine: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,8 +276,8 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
||||||
const ptB = Vec.sub(B, point)
|
const ptB = Vec.sub(B, point)
|
||||||
const bounds = this.getBounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
if (points.length <= 2) {
|
if (bounds.width < 8 && bounds.height < 8) {
|
||||||
return Vec.distanceToLineSegment(A, B, shape.point) < 4
|
return Vec.distanceToLineSegment(A, B, Utils.getBoundsCenter(bounds)) < 5
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intersectLineSegmentBounds(ptA, ptB, bounds)) {
|
if (intersectLineSegmentBounds(ptA, ptB, bounds)) {
|
||||||
|
|
|
@ -105,6 +105,7 @@ export interface TDSnapshot {
|
||||||
isMenuOpen: boolean
|
isMenuOpen: boolean
|
||||||
status: string
|
status: string
|
||||||
snapLines: TLSnapLine[]
|
snapLines: TLSnapLine[]
|
||||||
|
eraseLine: number[][]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
disableAssets: boolean
|
disableAssets: boolean
|
||||||
selectByContain?: boolean
|
selectByContain?: boolean
|
||||||
|
|
|
@ -9029,6 +9029,11 @@ perfect-freehand@^1.0.16:
|
||||||
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.16.tgz#38575ef946ff513b9c94057c763cac003b504020"
|
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.16.tgz#38575ef946ff513b9c94057c763cac003b504020"
|
||||||
integrity sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ==
|
integrity sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ==
|
||||||
|
|
||||||
|
perfect-freehand@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.1.0.tgz#65b459f4d6ccceb873e9ac06e07c924761649b32"
|
||||||
|
integrity sha512-nVWukMN9qlii1dQsQHVvfaNpeOAWVLgTZP6e/tFcU6cWlLo+6YdvfRGBL2u5pU11APlPbHeB0SpMcGA8ZjPgcQ==
|
||||||
|
|
||||||
picocolors@^1.0.0:
|
picocolors@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
|
Loading…
Reference in a new issue