diff --git a/examples/core-example-advanced/package.json b/examples/core-example-advanced/package.json index 6c16c92f9..bd6192da2 100644 --- a/examples/core-example-advanced/package.json +++ b/examples/core-example-advanced/package.json @@ -32,7 +32,7 @@ "immer": "^9.0.12", "lodash": "^4.17.21", "nanoid": "^3.1.31", - "perfect-freehand": "^1.0.16", + "perfect-freehand": "^1.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-feather": "^2.0.9", diff --git a/packages/core/package.json b/packages/core/package.json index 10efbbd9d..fff53f2b0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -41,6 +41,7 @@ "@tldraw/vec": "^1.7.0", "@use-gesture/react": "^10.2.14", "mobx-react-lite": "^3.2.3", + "perfect-freehand": "^1.1.0", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { diff --git a/packages/core/src/components/Canvas/Canvas.tsx b/packages/core/src/components/Canvas/Canvas.tsx index 14a776645..1642a7100 100644 --- a/packages/core/src/components/Canvas/Canvas.tsx +++ b/packages/core/src/components/Canvas/Canvas.tsx @@ -30,12 +30,14 @@ import { UsersIndicators } from '~components/UsersIndicators' import { SnapLines } from '~components/SnapLines/SnapLines' import { Grid } from '~components/Grid' import { Overlay } from '~components/Overlay' +import { EraseLine } from '~components/EraseLine' interface CanvasProps> { page: TLPage pageState: TLPageState assets: TLAssets snapLines?: TLSnapLine[] + eraseLine?: number[][] grid?: number users?: TLUsers userId?: string @@ -64,6 +66,7 @@ export const Canvas = observer(function _Canvas< pageState, assets, snapLines, + eraseLine, grid, users, userId, @@ -134,6 +137,7 @@ export const Canvas = observer(function _Canvas< {users && } + {eraseLine && } {snapLines && } diff --git a/packages/core/src/components/EraseLine/EraseLine.tsx b/packages/core/src/components/EraseLine/EraseLine.tsx new file mode 100644 index 000000000..a5c2ddcb2 --- /dev/null +++ b/packages/core/src/components/EraseLine/EraseLine.tsx @@ -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 +}) diff --git a/packages/core/src/components/EraseLine/index.ts b/packages/core/src/components/EraseLine/index.ts new file mode 100644 index 000000000..c1f97ae5b --- /dev/null +++ b/packages/core/src/components/EraseLine/index.ts @@ -0,0 +1 @@ +export * from './EraseLine' diff --git a/packages/core/src/components/Renderer/Renderer.tsx b/packages/core/src/components/Renderer/Renderer.tsx index 99bc3bdd9..3f2457cc3 100644 --- a/packages/core/src/components/Renderer/Renderer.tsx +++ b/packages/core/src/components/Renderer/Renderer.tsx @@ -56,6 +56,10 @@ export interface RendererProps extends Partial, selector?: string) { diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 1849bd0ae..40262e06f 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -55,7 +55,7 @@ "@types/lz-string": "^1.3.34", "idb-keyval": "^6.1.0", "lz-string": "^1.4.4", - "perfect-freehand": "^1.0.16", + "perfect-freehand": "^1.1.0", "react-error-boundary": "^3.1.4", "react-hotkey-hook": "^1.0.2", "react-hotkeys-hook": "^3.4.4", diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index f1b71f6f4..93ea412fd 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -431,6 +431,7 @@ const InnerTldraw = React.memo(function InnerTldraw({ pageState={pageState} assets={assets} snapLines={appState.snapLines} + eraseLine={appState.eraseLine} grid={GRID_SIZE} users={room?.users} userId={room?.userId} diff --git a/packages/tldraw/src/state/StateManager/StateManager.ts b/packages/tldraw/src/state/StateManager/StateManager.ts index 87db0de2e..3cbab3874 100644 --- a/packages/tldraw/src/state/StateManager/StateManager.ts +++ b/packages/tldraw/src/state/StateManager/StateManager.ts @@ -198,7 +198,7 @@ export class StateManager> { * @param patch The patch to apply. * @param id (optional) An id for this patch. */ - protected patchState = (patch: Patch, id?: string): this => { + patchState = (patch: Patch, id?: string): this => { this.applyPatch(patch, id) if (this.onPatch) { this.onPatch(this._state, id) diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index c58cdcb3e..6338121c9 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -4103,6 +4103,7 @@ export class TldrawApp extends StateManager { isToolLocked: false, isMenuOpen: false, isEmptyCanvas: false, + eraseLine: [], snapLines: [], isLoading: false, disableAssets: false, diff --git a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts index 9b8731267..ba8fa24a6 100644 --- a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts +++ b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts @@ -22,12 +22,56 @@ export class EraseSession extends BaseSession { initialSelectedShapes: TDShape[] erasableShapes: Set prevPoint: number[] + prevEraseShapesSize = 0 constructor(app: TldrawApp) { super(app) this.prevPoint = [...app.originPoint] this.initialSelectedShapes = this.app.selectedIds.map((id) => this.app.getShape(id)) 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 @@ -100,6 +144,12 @@ export class EraseSession extends BaseSession { this.prevPoint = newPoint + if (erasedShapes.length === this.prevEraseShapesSize) { + return + } + + this.prevEraseShapesSize = erasedShapes.length + return { document: { pages: { @@ -114,6 +164,8 @@ export class EraseSession extends BaseSession { cancel = (): TldrawPatch | undefined => { const { page } = this.app + cancelAnimationFrame(this.interval) + this.erasedShapes.forEach((shape) => { if (!this.app.getShape(shape.id)) { this.erasedShapes.delete(shape) @@ -136,12 +188,17 @@ export class EraseSession extends BaseSession { }, }, }, + appState: { + eraseLine: [], + }, } } complete = (): TldrawPatch | TldrawCommand | undefined => { const { page } = this.app + cancelAnimationFrame(this.interval) + this.erasedShapes.forEach((shape) => { if (!this.app.getShape(shape.id)) { this.erasedShapes.delete(shape) @@ -217,6 +274,9 @@ export class EraseSession extends BaseSession { }, }, }, + appState: { + eraseLine: [], + }, }, after: { document: { @@ -232,6 +292,9 @@ export class EraseSession extends BaseSession { }, }, }, + appState: { + eraseLine: [], + }, }, } } diff --git a/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx b/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx index 08196d89c..e33cc77f0 100644 --- a/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx +++ b/packages/tldraw/src/state/shapes/DrawUtil/DrawUtil.tsx @@ -276,8 +276,8 @@ export class DrawUtil extends TDShapeUtil { const ptB = Vec.sub(B, point) const bounds = this.getBounds(shape) - if (points.length <= 2) { - return Vec.distanceToLineSegment(A, B, shape.point) < 4 + if (bounds.width < 8 && bounds.height < 8) { + return Vec.distanceToLineSegment(A, B, Utils.getBoundsCenter(bounds)) < 5 } if (intersectLineSegmentBounds(ptA, ptB, bounds)) { diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index a5de39dd1..59ec88bf1 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -105,6 +105,7 @@ export interface TDSnapshot { isMenuOpen: boolean status: string snapLines: TLSnapLine[] + eraseLine: number[][] isLoading: boolean disableAssets: boolean selectByContain?: boolean diff --git a/yarn.lock b/yarn.lock index 841734971..4235ae3ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9029,6 +9029,11 @@ perfect-freehand@^1.0.16: resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.16.tgz#38575ef946ff513b9c94057c763cac003b504020" 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: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"