kopia lustrzana https://github.com/Tldraw/Tldraw
238 wiersze
6.6 KiB
TypeScript
238 wiersze
6.6 KiB
TypeScript
import {
|
|
TLArrowBinding,
|
|
TLArrowBindingProps,
|
|
TLArrowShape,
|
|
TLShape,
|
|
TLShapeId,
|
|
} from '@tldraw/tlschema'
|
|
import { Mat } from '../../../../primitives/Mat'
|
|
import { Vec } from '../../../../primitives/Vec'
|
|
import { Group2d } from '../../../../primitives/geometry/Group2d'
|
|
import { Editor } from '../../../Editor'
|
|
|
|
export function getIsArrowStraight(shape: TLArrowShape) {
|
|
return Math.abs(shape.props.bend) < 8 // snap to +-8px
|
|
}
|
|
|
|
export type BoundShapeInfo<T extends TLShape = TLShape> = {
|
|
shape: T
|
|
didIntersect: boolean
|
|
isExact: boolean
|
|
isClosed: boolean
|
|
transform: Mat
|
|
outline: Vec[]
|
|
}
|
|
|
|
export function getBoundShapeInfoForTerminal(
|
|
editor: Editor,
|
|
arrow: TLArrowShape,
|
|
terminalName: 'start' | 'end'
|
|
): BoundShapeInfo | undefined {
|
|
const binding = editor
|
|
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
|
|
.find((b) => b.props.terminal === terminalName)
|
|
if (!binding) return
|
|
|
|
const boundShape = editor.getShape(binding.toId)!
|
|
const transform = editor.getShapePageTransform(boundShape)!
|
|
const geometry = editor.getShapeGeometry(boundShape)
|
|
|
|
// This is hacky: we're only looking at the first child in the group. Really the arrow should
|
|
// consider all items in the group which are marked as snappable as separate polygons with which
|
|
// to intersect, in the case of a group that has multiple children which do not overlap; or else
|
|
// flatten the geometry into a set of polygons and intersect with that.
|
|
const outline = geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
|
|
|
|
return {
|
|
shape: boundShape,
|
|
transform,
|
|
isClosed: geometry.isClosed,
|
|
isExact: binding.props.isExact,
|
|
didIntersect: false,
|
|
outline,
|
|
}
|
|
}
|
|
|
|
function getArrowTerminalInArrowSpace(
|
|
editor: Editor,
|
|
arrowPageTransform: Mat,
|
|
binding: TLArrowBinding,
|
|
forceImprecise: boolean
|
|
) {
|
|
const boundShape = editor.getShape(binding.toId)
|
|
|
|
if (!boundShape) {
|
|
// this can happen in multiplayer contexts where the shape is being deleted
|
|
return new Vec(0, 0)
|
|
} else {
|
|
// Find the actual local point of the normalized terminal on
|
|
// the bound shape and transform it to page space, then transform
|
|
// it to arrow space
|
|
const { point, size } = editor.getShapeGeometry(boundShape).bounds
|
|
const shapePoint = Vec.Add(
|
|
point,
|
|
Vec.MulV(
|
|
// if the parent is the bound shape, then it's ALWAYS precise
|
|
binding.props.isPrecise || forceImprecise
|
|
? binding.props.normalizedAnchor
|
|
: { x: 0.5, y: 0.5 },
|
|
size
|
|
)
|
|
)
|
|
const pagePoint = Mat.applyToPoint(editor.getShapePageTransform(boundShape)!, shapePoint)
|
|
const arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pagePoint)
|
|
return arrowPoint
|
|
}
|
|
}
|
|
|
|
export interface TLArrowBindings {
|
|
start: TLArrowBinding | undefined
|
|
end: TLArrowBinding | undefined
|
|
}
|
|
|
|
/** @internal */
|
|
export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings {
|
|
const bindings = editor.getBindingsFromShape<TLArrowBinding>(shape, 'arrow')
|
|
return {
|
|
start: bindings.find((b) => b.props.terminal === 'start'),
|
|
end: bindings.find((b) => b.props.terminal === 'end'),
|
|
}
|
|
}
|
|
|
|
/** @public */
|
|
export function getArrowTerminalsInArrowSpace(
|
|
editor: Editor,
|
|
shape: TLArrowShape,
|
|
bindings: TLArrowBindings
|
|
) {
|
|
const arrowPageTransform = editor.getShapePageTransform(shape)!
|
|
|
|
const boundShapeRelationships = getBoundShapeRelationships(
|
|
editor,
|
|
bindings.start?.toId,
|
|
bindings.end?.toId
|
|
)
|
|
|
|
const start = bindings.start
|
|
? getArrowTerminalInArrowSpace(
|
|
editor,
|
|
arrowPageTransform,
|
|
bindings.start,
|
|
boundShapeRelationships === 'double-bound' ||
|
|
boundShapeRelationships === 'start-contains-end'
|
|
)
|
|
: Vec.From(shape.props.start)
|
|
|
|
const end = bindings.end
|
|
? getArrowTerminalInArrowSpace(
|
|
editor,
|
|
arrowPageTransform,
|
|
bindings.end,
|
|
boundShapeRelationships === 'double-bound' ||
|
|
boundShapeRelationships === 'end-contains-start'
|
|
)
|
|
: Vec.From(shape.props.end)
|
|
|
|
return { start, end }
|
|
}
|
|
|
|
/**
|
|
* Create or update the arrow binding for a particular arrow terminal. Will clear up if needed.
|
|
* TODO(alex): find a better name for this
|
|
* @internal
|
|
*/
|
|
export function arrowBindingMakeItSo(
|
|
editor: Editor,
|
|
arrow: TLArrowShape | TLShapeId,
|
|
target: TLShape | TLShapeId,
|
|
props: TLArrowBindingProps
|
|
) {
|
|
const arrowId = typeof arrow === 'string' ? arrow : arrow.id
|
|
const targetId = typeof target === 'string' ? target : target.id
|
|
|
|
const existingMany = editor
|
|
.getBindingsFromShape<TLArrowBinding>(arrowId, 'arrow')
|
|
.filter((b) => b.props.terminal === props.terminal)
|
|
|
|
// if we've somehow ended up with too many bindings, delete the extras
|
|
if (existingMany.length > 1) {
|
|
editor.deleteBindings(existingMany.slice(1))
|
|
}
|
|
|
|
const existing = existingMany[0]
|
|
if (existing) {
|
|
editor.updateBinding({
|
|
...existing,
|
|
toId: targetId,
|
|
props,
|
|
})
|
|
} else {
|
|
editor.createBinding({
|
|
type: 'arrow',
|
|
fromId: arrowId,
|
|
toId: targetId,
|
|
props,
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove any arrow bindings for a particular terminal.
|
|
* @internal
|
|
*/
|
|
export function arrowBindingMakeItNotSo(
|
|
editor: Editor,
|
|
arrow: TLArrowShape,
|
|
terminal: 'start' | 'end'
|
|
) {
|
|
const existing = editor
|
|
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
|
|
.filter((b) => b.props.terminal === terminal)
|
|
|
|
editor.deleteBindings(existing)
|
|
}
|
|
|
|
/** @internal */
|
|
export const MIN_ARROW_LENGTH = 10
|
|
/** @internal */
|
|
export const BOUND_ARROW_OFFSET = 10
|
|
/** @internal */
|
|
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
|
|
|
/** @public */
|
|
export const STROKE_SIZES: Record<string, number> = {
|
|
s: 2,
|
|
m: 3.5,
|
|
l: 5,
|
|
xl: 10,
|
|
}
|
|
|
|
/**
|
|
* Get the relationships for an arrow that has two bound shape terminals.
|
|
* If the arrow has only one bound shape, then it is always "safe" to apply
|
|
* standard offsets and precision behavior. If the shape is bound to the same
|
|
* shape on both ends, then that is an exception. If one of the shape's
|
|
* terminals is bound to a shape that contains / is contained by the shape that
|
|
* is bound to the other terminal, then that is also an exception.
|
|
*
|
|
* @param editor - the editor instance
|
|
* @param startShapeId - the bound shape from the arrow's start
|
|
* @param endShapeId - the bound shape from the arrow's end
|
|
*
|
|
* @internal */
|
|
export function getBoundShapeRelationships(
|
|
editor: Editor,
|
|
startShapeId?: TLShapeId,
|
|
endShapeId?: TLShapeId
|
|
) {
|
|
if (!startShapeId || !endShapeId) return 'safe'
|
|
if (startShapeId === endShapeId) return 'double-bound'
|
|
const startBounds = editor.getShapePageBounds(startShapeId)
|
|
const endBounds = editor.getShapePageBounds(endShapeId)
|
|
if (startBounds && endBounds) {
|
|
if (startBounds.contains(endBounds)) return 'start-contains-end'
|
|
if (endBounds.contains(startBounds)) return 'end-contains-start'
|
|
}
|
|
return 'safe'
|
|
}
|