kopia lustrzana https://github.com/Tldraw/Tldraw
151 wiersze
4.2 KiB
TypeScript
151 wiersze
4.2 KiB
TypeScript
import {
|
|
Box,
|
|
HIT_TEST_MARGIN,
|
|
StateNode,
|
|
TLEventHandlers,
|
|
TLFrameShape,
|
|
TLGroupShape,
|
|
TLPointerEventInfo,
|
|
TLShapeId,
|
|
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>()
|
|
|
|
override onEnter = (info: TLPointerEventInfo) => {
|
|
this.markId = 'erase scribble begin'
|
|
this.editor.mark(this.markId)
|
|
this.info = info
|
|
|
|
const { originPagePoint } = this.editor.inputs
|
|
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<TLGroupShape>(shape, 'group') ||
|
|
this.editor.isShapeOfType<TLFrameShape>(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.currentPagePoint
|
|
this.editor.scribbles.addPoint(this.scribbleId, x, y)
|
|
}
|
|
|
|
override onExit = () => {
|
|
this.editor.scribbles.stop(this.scribbleId)
|
|
}
|
|
|
|
override onPointerMove = () => {
|
|
this.update()
|
|
}
|
|
|
|
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
|
this.complete()
|
|
}
|
|
|
|
override onCancel: TLEventHandlers['onCancel'] = () => {
|
|
this.cancel()
|
|
}
|
|
|
|
override onComplete: TLEventHandlers['onComplete'] = () => {
|
|
this.complete()
|
|
}
|
|
|
|
update() {
|
|
const { editor, excludedShapeIds } = this
|
|
const erasingShapeIds = this.editor.getErasingShapeIds()
|
|
const zoomLevel = this.editor.getZoomLevel()
|
|
const {
|
|
inputs: { currentPagePoint, previousPagePoint },
|
|
} = editor
|
|
|
|
this.pushPointToScribble()
|
|
|
|
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
|
const minDist = HIT_TEST_MARGIN / zoomLevel
|
|
|
|
const shapesNearPoint = this.editor.getShapesInsideBounds(
|
|
Box.FromPoints([currentPagePoint, previousPagePoint]).expandBy(minDist)
|
|
)
|
|
for (const shape of shapesNearPoint) {
|
|
if (this.editor.isShapeOfType<TLGroupShape>(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)
|
|
}
|
|
}
|
|
|
|
// 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([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
|
}
|
|
|
|
complete() {
|
|
const { editor } = this
|
|
editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds)
|
|
editor.setErasingShapes([])
|
|
this.parent.transition('idle')
|
|
}
|
|
|
|
cancel() {
|
|
const { editor } = this
|
|
editor.setErasingShapes([])
|
|
editor.bailToMark(this.markId)
|
|
this.parent.transition('idle', this.info)
|
|
}
|
|
}
|