Tldraw/packages/editor/src/lib/editor/managers/SnapManager/SnapManager.ts

110 wiersze
3.1 KiB
TypeScript

import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
import { TLFrameShape, TLGroupShape, TLParentId, TLShapeId, isShapeId } from '@tldraw/tlschema'
import { Vec, VecLike } from '../../../primitives/Vec'
import type { Editor } from '../../Editor'
import { BoundsSnaps } from './BoundsSnaps'
import { HandleSnaps } from './HandleSnaps'
/** @public */
export type PointsSnapIndicator = {
id: string
type: 'points'
points: VecLike[]
}
/** @public */
export type GapsSnapIndicator = {
id: string
type: 'gaps'
direction: 'horizontal' | 'vertical'
gaps: Array<{
startEdge: [VecLike, VecLike]
endEdge: [VecLike, VecLike]
}>
}
/** @public */
export type SnapIndicator = PointsSnapIndicator | GapsSnapIndicator
/** @public */
export interface SnapData {
nudge: Vec
}
/** @public */
export class SnapManager {
readonly shapeBounds: BoundsSnaps
readonly handles: HandleSnaps
private _snapIndicators = atom<SnapIndicator[] | undefined>('snapLines', undefined)
constructor(public readonly editor: Editor) {
this.shapeBounds = new BoundsSnaps(this)
this.handles = new HandleSnaps(this)
}
getIndicators() {
return this._snapIndicators.get() ?? (EMPTY_ARRAY as SnapIndicator[])
}
clearIndicators() {
if (this.getIndicators().length) {
this._snapIndicators.set(undefined)
}
}
setIndicators(indicators: SnapIndicator[]) {
this._snapIndicators.set(indicators)
}
@computed getSnapThreshold() {
return 8 / this.editor.getZoomLevel()
}
// TODO: make this an incremental derivation
@computed getSnappableShapes(): Set<TLShapeId> {
const { editor } = this
const renderingBounds = editor.getViewportPageBounds()
const selectedShapeIds = editor.getSelectedShapeIds()
const snappableShapes: Set<TLShapeId> = new Set()
const collectSnappableShapesFromParent = (parentId: TLParentId) => {
if (isShapeId(parentId)) {
const parent = editor.getShape(parentId)
if (parent && editor.isShapeOfType<TLFrameShape>(parent, 'frame')) {
snappableShapes.add(parentId)
}
}
const sortedChildIds = editor.getSortedChildIdsForParent(parentId)
for (const childId of sortedChildIds) {
// Skip any selected ids
if (selectedShapeIds.includes(childId)) continue
const childShape = editor.getShape(childId)
if (!childShape) continue
const util = editor.getShapeUtil(childShape)
// Skip any shapes that don't allow snapping
if (!util.canSnap(childShape)) continue
// Only consider shapes if they're inside of the viewport page bounds
const pageBounds = editor.getShapePageBounds(childId)
if (!(pageBounds && renderingBounds.includes(pageBounds))) continue
// Snap to children of groups but not group itself
if (editor.isShapeOfType<TLGroupShape>(childShape, 'group')) {
collectSnappableShapesFromParent(childId)
continue
}
snappableShapes.add(childId)
}
}
collectSnappableShapesFromParent(this.getCurrentCommonAncestor() ?? editor.getCurrentPageId())
return snappableShapes
}
// This needs to be external from any expensive work
@computed getCurrentCommonAncestor() {
return this.editor.findCommonAncestor(this.editor.getSelectedShapes())
}
}