kopia lustrzana https://github.com/Tldraw/Tldraw
261 wiersze
6.4 KiB
TypeScript
261 wiersze
6.4 KiB
TypeScript
import {
|
|
SelectionHandle,
|
|
StateNode,
|
|
TLBaseShape,
|
|
TLEnterEventHandler,
|
|
TLEventHandlers,
|
|
TLImageShape,
|
|
TLImageShapeCrop,
|
|
TLPointerEventInfo,
|
|
TLShapePartial,
|
|
Vec,
|
|
structuredClone,
|
|
} from '@tldraw/editor'
|
|
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
|
import { CursorTypeMap } from './PointingResizeHandle'
|
|
|
|
type Snapshot = ReturnType<Cropping['createSnapshot']>
|
|
|
|
export class Cropping extends StateNode {
|
|
static override id = 'cropping'
|
|
|
|
info = {} as TLPointerEventInfo & {
|
|
target: 'selection'
|
|
handle: SelectionHandle
|
|
onInteractionEnd?: string
|
|
}
|
|
|
|
markId = ''
|
|
isDirty = false
|
|
|
|
private snapshot = {} as any as Snapshot
|
|
|
|
override onEnter: TLEnterEventHandler = (
|
|
info: TLPointerEventInfo & {
|
|
target: 'selection'
|
|
handle: SelectionHandle
|
|
onInteractionEnd?: string
|
|
}
|
|
) => {
|
|
this.info = info
|
|
this.markId = 'cropping'
|
|
this.editor.mark(this.markId)
|
|
this.snapshot = this.createSnapshot()
|
|
this.isDirty = false
|
|
this.updateShapes()
|
|
}
|
|
|
|
override onTick = () => {
|
|
if (this.isDirty) {
|
|
this.isDirty = false
|
|
this.updateShapes()
|
|
}
|
|
}
|
|
|
|
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
|
this.isDirty = true
|
|
}
|
|
|
|
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
|
this.complete()
|
|
}
|
|
|
|
override onComplete: TLEventHandlers['onComplete'] = () => {
|
|
this.complete()
|
|
}
|
|
|
|
override onCancel: TLEventHandlers['onCancel'] = () => {
|
|
this.cancel()
|
|
}
|
|
|
|
private updateCursor() {
|
|
const selectedShape = this.editor.getSelectedShapes()[0]
|
|
if (!selectedShape) return
|
|
|
|
const cursorType = CursorTypeMap[this.info.handle!]
|
|
this.editor.updateInstanceState({
|
|
cursor: {
|
|
type: cursorType,
|
|
rotation: this.editor.getSelectionRotation(),
|
|
},
|
|
})
|
|
}
|
|
|
|
private getDefaultCrop = (): TLImageShapeCrop => ({
|
|
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], { squashing: true })
|
|
this.updateCursor()
|
|
}
|
|
|
|
private complete() {
|
|
this.updateShapes()
|
|
this.isDirty = false
|
|
if (this.info.onInteractionEnd) {
|
|
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
|
} else {
|
|
this.editor.setCroppingShape(null)
|
|
this.parent.transition('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.parent.transition('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,
|
|
}
|
|
}
|
|
}
|