UNPKG

tldraw

Version:

A tiny little drawing editor.

187 lines (153 loc) 5.63 kB
import { Box, StateNode, TLPointerEventInfo, TLShapeId, isAccelKey, pointInPolygon, } from '@tldraw/editor' export class Erasing extends StateNode { static override id = 'erasing' private info = {} as TLPointerEventInfo private scribbleId = 'id' private markId = '' private excludedShapeIds = new Set<TLShapeId>() _isHoldingAccelKey = false _firstErasingShapeId: TLShapeId | null = null _erasingShapeIds: TLShapeId[] = [] override onEnter(info: TLPointerEventInfo) { this._isHoldingAccelKey = isAccelKey(this.editor.inputs) this._firstErasingShapeId = this.editor.getErasingShapeIds()[0] // the first one should be the first one we hit... is it? this._erasingShapeIds = this.editor.getErasingShapeIds() this.markId = this.editor.markHistoryStoppingPoint('erase scribble begin') this.info = info const originPagePoint = this.editor.inputs.getOriginPagePoint() this.excludedShapeIds = new Set( this.editor .getCurrentPageShapes() .filter((shape) => { //If the shape is locked, we shouldn't erase it if (this.editor.isShapeOrAncestorLocked(shape)) return true //If the shape is a group or frame, check we're inside it when we start erasing if ( this.editor.isShapeOfType(shape, 'group') || this.editor.isShapeOfType(shape, 'frame') ) { const pointInShapeShape = this.editor.getPointInShapeSpace(shape, originPagePoint) const geometry = this.editor.getShapeGeometry(shape) return geometry.bounds.containsPoint(pointInShapeShape) } return false }) .map((shape) => shape.id) ) const scribble = this.editor.scribbles.addScribble({ color: 'muted-1', size: 12, }) this.scribbleId = scribble.id this.update() } private pushPointToScribble() { const { x, y } = this.editor.inputs.getCurrentPagePoint() this.editor.scribbles.addPoint(this.scribbleId, x, y) } override onExit() { this.editor.setErasingShapes([]) this.editor.scribbles.stop(this.scribbleId) } override onPointerMove() { this.update() } override onPointerUp() { this.complete() } override onCancel() { this.cancel() } override onComplete() { this.complete() } override onKeyUp() { this._isHoldingAccelKey = isAccelKey(this.editor.inputs) this.update() } override onKeyDown() { this._isHoldingAccelKey = isAccelKey(this.editor.inputs) this.update() } update() { const { editor, excludedShapeIds } = this const erasingShapeIds = editor.getErasingShapeIds() const zoomLevel = editor.getZoomLevel() const currentPagePoint = editor.inputs.getCurrentPagePoint() const previousPagePoint = editor.inputs.getPreviousPagePoint() this.pushPointToScribble() // Otherwise, erasing shapes are all the shapes that were hit before plus any new shapes that are hit const erasing = new Set<TLShapeId>(erasingShapeIds) const minDist = this.editor.options.hitTestMargin / zoomLevel // Create bounds around line segment with margin const lineBounds = Box.FromPoints([previousPagePoint, currentPagePoint]).expandBy(minDist) const candidateIds = editor.getShapeIdsInsideBounds(lineBounds) // Early return if no candidates - avoid expensive getCurrentPageRenderingShapesSorted() if (candidateIds.size === 0) { editor.setErasingShapes(Array.from(erasing)) return } const allShapes = editor.getCurrentPageRenderingShapesSorted() const currentPageShapes = allShapes.filter((shape) => candidateIds.has(shape.id)) for (const shape of currentPageShapes) { if (editor.isShapeOfType(shape, 'group')) continue // Avoid testing masked shapes, unless the pointer is inside the mask const pageMask = editor.getShapeMask(shape.id) if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) { continue } // Hit test the shape using a line segment const geometry = editor.getShapeGeometry(shape) const pageTransform = editor.getShapePageTransform(shape) if (!geometry || !pageTransform) continue const pt = pageTransform.clone().invert() const A = pt.applyToPoint(previousPagePoint) const B = pt.applyToPoint(currentPagePoint) // If the line segment is entirely above / below / left / right of the shape's bounding box, skip the hit test const { bounds } = geometry if ( bounds.minX - minDist > Math.max(A.x, B.x) || bounds.minY - minDist > Math.max(A.y, B.y) || bounds.maxX + minDist < Math.min(A.x, B.x) || bounds.maxY + minDist < Math.min(A.y, B.y) ) { continue } if (geometry.hitTestLineSegment(A, B, minDist)) { erasing.add(editor.getOutermostSelectableShape(shape).id) } this._erasingShapeIds = [...erasing] } // If the user is holding the meta / ctrl key, we should only erase the first shape we hit if (this._isHoldingAccelKey && this._firstErasingShapeId) { const erasingShapeId = this._firstErasingShapeId if (erasingShapeId && this.editor.getShape(erasingShapeId)) { editor.setErasingShapes([erasingShapeId]) } return } // Remove the hit shapes, except if they're in the list of excluded shapes // (these excluded shapes will be any frames or groups the pointer was inside of // when the user started erasing) this.editor.setErasingShapes(this._erasingShapeIds.filter((id) => !excludedShapeIds.has(id))) } complete() { const { editor } = this editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds) this.parent.transition('idle') this._erasingShapeIds = [] this._firstErasingShapeId = null } cancel() { const { editor } = this editor.bailToMark(this.markId) this.parent.transition('idle', this.info) } }