UNPKG

tldraw

Version:

A tiny little drawing editor.

247 lines (207 loc) • 6.24 kB
import { SelectionHandle, StateNode, TLBaseShape, TLImageShape, TLImageShapeCrop, TLPointerEventInfo, TLShapePartial, Vec, structuredClone, } from '@tldraw/editor' import { kickoutOccludedShapes } from '../../../selectHelpers' import { CursorTypeMap } from '../../PointingResizeHandle' import { MIN_CROP_SIZE } from '../crop-constants' type Snapshot = ReturnType<Cropping['createSnapshot']> export class Cropping extends StateNode { static override id = 'cropping' info = {} as TLPointerEventInfo & { target: 'selection' handle: SelectionHandle onInteractionEnd?: string } markId = '' private snapshot = {} as any as Snapshot override onEnter( info: TLPointerEventInfo & { target: 'selection' handle: SelectionHandle onInteractionEnd?: string } ) { this.info = info this.markId = this.editor.markHistoryStoppingPoint('cropping') this.snapshot = this.createSnapshot() this.updateShapes() } override onPointerMove() { this.updateShapes() } override onPointerUp() { this.complete() } override onComplete() { this.complete() } override onCancel() { this.cancel() } private updateCursor() { const selectedShape = this.editor.getSelectedShapes()[0] if (!selectedShape) return const cursorType = CursorTypeMap[this.info.handle!] this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() }) } getDefaultCrop() { return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, } } private updateShapes() { const { shape, cursorHandleOffset } = this.snapshot if (!shape) return const util = this.editor.getShapeUtil<TLImageShape>('image') if (!util) return const props = shape.props const currentPagePoint = this.editor.inputs.currentPagePoint.clone().sub(cursorHandleOffset) const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset) const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation) const crop = props.crop ?? this.getDefaultCrop() const newCrop = structuredClone(crop) const newPoint = new Vec(shape.x, shape.y) const pointDelta = new Vec(0, 0) // original (uncropped) width and height of shape const w = (1 / (crop.bottomRight.x - crop.topLeft.x)) * props.w const h = (1 / (crop.bottomRight.y - crop.topLeft.y)) * props.h let hasCropChanged = false // Set y dimension switch (this.info.handle) { case 'top': case 'top_left': case 'top_right': { if (h < MIN_CROP_SIZE) break hasCropChanged = true // top newCrop.topLeft.y = newCrop.topLeft.y + change.y / h const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y) if (heightAfterCrop < MIN_CROP_SIZE) { newCrop.topLeft.y = newCrop.bottomRight.y - MIN_CROP_SIZE / h pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h } else { if (newCrop.topLeft.y <= 0) { newCrop.topLeft.y = 0 pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h } else { pointDelta.y = change.y } } break } case 'bottom': case 'bottom_left': case 'bottom_right': { if (h < MIN_CROP_SIZE) break hasCropChanged = true // bottom newCrop.bottomRight.y = Math.min(1, newCrop.bottomRight.y + change.y / h) const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y) if (heightAfterCrop < MIN_CROP_SIZE) { newCrop.bottomRight.y = newCrop.topLeft.y + MIN_CROP_SIZE / h } break } } // Set x dimension switch (this.info.handle) { case 'left': case 'top_left': case 'bottom_left': { if (w < MIN_CROP_SIZE) break hasCropChanged = true // left newCrop.topLeft.x = newCrop.topLeft.x + change.x / w const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x) if (widthAfterCrop < MIN_CROP_SIZE) { newCrop.topLeft.x = newCrop.bottomRight.x - MIN_CROP_SIZE / w pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w } else { if (newCrop.topLeft.x <= 0) { newCrop.topLeft.x = 0 pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w } else { pointDelta.x = change.x } } break } case 'right': case 'top_right': case 'bottom_right': { if (w < MIN_CROP_SIZE) break hasCropChanged = true // right newCrop.bottomRight.x = Math.min(1, newCrop.bottomRight.x + change.x / w) const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x) if (widthAfterCrop < MIN_CROP_SIZE) { newCrop.bottomRight.x = newCrop.topLeft.x + MIN_CROP_SIZE / w } break } } if (!hasCropChanged) return newPoint.add(pointDelta.rot(shape.rotation)) const partial: TLShapePartial< TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }> > = { id: shape.id, type: shape.type, x: newPoint.x, y: newPoint.y, props: { crop: newCrop, w: (newCrop.bottomRight.x - newCrop.topLeft.x) * w, h: (newCrop.bottomRight.y - newCrop.topLeft.y) * h, }, } this.editor.updateShapes([partial]) this.updateCursor() } private complete() { this.updateShapes() kickoutOccludedShapes(this.editor, [this.snapshot.shape.id]) if (this.info.onInteractionEnd) { this.editor.setCurrentTool(this.info.onInteractionEnd, this.info) } else { this.editor.setCroppingShape(null) this.editor.setCurrentTool('select.idle') } } private cancel() { this.editor.bailToMark(this.markId) if (this.info.onInteractionEnd) { this.editor.setCurrentTool(this.info.onInteractionEnd, this.info) } else { this.editor.setCroppingShape(null) this.editor.setCurrentTool('select.idle') } } private createSnapshot() { const selectionRotation = this.editor.getSelectionRotation() const { inputs: { originPagePoint }, } = this.editor const shape = this.editor.getOnlySelectedShape() as TLImageShape const selectionBounds = this.editor.getSelectionRotatedPageBounds()! const dragHandlePoint = Vec.RotWith( selectionBounds.getHandlePoint(this.info.handle!), selectionBounds.point, selectionRotation ) const cursorHandleOffset = Vec.Sub(originPagePoint, dragHandlePoint) return { shape, cursorHandleOffset, } } }