UNPKG

tldraw

Version:

A tiny little drawing editor.

252 lines (251 loc) • 8.74 kB
import { ArrowShapeKindStyle, atom, Box, clamp, Geometry2dFilters, invLerp, mapObjectMapValues, objectMapEntries, objectMapKeys, Vec, WeakCache } from "@tldraw/editor"; import { ElbowArrowAxes, ElbowArrowSideAxes, ElbowArrowSideDeltas } from "./elbow/definitions.mjs"; const arrowTargetStore = new WeakCache(); function getArrowTargetAtom(editor) { return arrowTargetStore.get(editor, () => atom("arrowTarget", null)); } function getArrowTargetState(editor) { return getArrowTargetAtom(editor).get(); } function clearArrowTargetState(editor) { getArrowTargetAtom(editor).set(null); } function updateArrowTargetState({ editor, pointInPageSpace, arrow, isPrecise, currentBinding, oppositeBinding }) { const util = editor.getShapeUtil("arrow"); if (util.options.shouldIgnoreTargets(editor)) { getArrowTargetAtom(editor).set(null); return null; } const arrowKind = arrow ? arrow.props.kind : editor.getStyleForNextShape(ArrowShapeKindStyle); const target = editor.getShapeAtPoint(pointInPageSpace, { hitInside: true, hitFrameInside: true, margin: arrowKind === "elbow" ? 8 : [8, 0], filter: (targetShape) => { return !targetShape.isLocked && editor.canBindShapes({ fromShape: arrow ?? targetFilterFallback, toShape: targetShape, binding: "arrow" }); } }); if (!target) { getArrowTargetAtom(editor).set(null); return null; } const targetGeometryInTargetSpace = editor.getShapeGeometry(target); const targetBoundsInTargetSpace = Box.ZeroFix(targetGeometryInTargetSpace.bounds); const targetCenterInTargetSpace = targetGeometryInTargetSpace.center; const targetTransform = editor.getShapePageTransform(target); const pointInTargetSpace = editor.getPointInShapeSpace(target, pointInPageSpace); const castDistance = Math.max( targetGeometryInTargetSpace.bounds.width, targetGeometryInTargetSpace.bounds.height ); const handlesInPageSpace = mapObjectMapValues(ElbowArrowSideDeltas, (side, delta) => { const axis = ElbowArrowAxes[ElbowArrowSideAxes[side]]; const farPoint = Vec.Mul(delta, castDistance).add(targetCenterInTargetSpace); let isEnabled = false; let handlePointInTargetSpace = axis.v( targetBoundsInTargetSpace[side], targetBoundsInTargetSpace[axis.crossMid] ); let furthestDistance = 0; const intersections = targetGeometryInTargetSpace.intersectLineSegment( targetCenterInTargetSpace, farPoint, Geometry2dFilters.EXCLUDE_NON_STANDARD ); for (const intersection of intersections) { const distance = Vec.Dist2(intersection, targetCenterInTargetSpace); if (distance > furthestDistance) { furthestDistance = distance; handlePointInTargetSpace = intersection; isEnabled = targetGeometryInTargetSpace.isClosed; } } const handlePointInPageSpace = targetTransform.applyToPoint(handlePointInTargetSpace); return { point: handlePointInPageSpace, isEnabled, far: targetTransform.applyToPoint(farPoint) }; }); const zoomLevel = editor.getZoomLevel(); const minDistScaled = util.options.minElbowHandleDistance / zoomLevel; const targetCenterInPageSpace = targetTransform.applyToPoint(targetCenterInTargetSpace); for (const side of objectMapKeys(handlesInPageSpace)) { const handle = handlesInPageSpace[side]; if (Vec.DistMin(handle.point, targetCenterInPageSpace, minDistScaled)) { handle.isEnabled = false; } } let precise = isPrecise; if (!precise) { if (!currentBinding || currentBinding && target.id !== currentBinding.toId) { precise = editor.inputs.getPointerVelocity().len() < 0.5; } } if (!isPrecise) { if (!targetGeometryInTargetSpace.isClosed) { precise = true; } if (oppositeBinding && target.id === oppositeBinding.toId && oppositeBinding.props.isPrecise) { precise = true; } } const isExact = util.options.shouldBeExact(editor, precise); if (isExact) precise = true; const shouldSnapCenter = !isExact && precise && targetGeometryInTargetSpace.isClosed; const shouldSnapEdges = !isExact && (precise && arrowKind === "elbow" || !targetGeometryInTargetSpace.isClosed); const shouldSnapEdgePoints = !isExact && precise && arrowKind === "elbow" && targetGeometryInTargetSpace.isClosed; const shouldSnapNone = precise && (targetGeometryInTargetSpace.isClosed || isExact); const shouldSnapCenterAxis = !isExact && precise && arrowKind === "elbow" && targetGeometryInTargetSpace.isClosed; let snap = "none"; let anchorInPageSpace = pointInPageSpace; if (!shouldSnapNone) { snap = "center"; anchorInPageSpace = targetCenterInPageSpace; } if (shouldSnapEdges) { const snapDistance = shouldSnapNone ? calculateSnapDistance( editor, targetBoundsInTargetSpace, util.options.elbowArrowEdgeSnapDistance ) : Infinity; const nearestPointOnEdgeInTargetSpace = targetGeometryInTargetSpace.nearestPoint( pointInTargetSpace, { includeLabels: false, includeInternal: false } ); const nearestPointOnEdgeInPageSpace = targetTransform.applyToPoint( nearestPointOnEdgeInTargetSpace ); const distance = Vec.Dist(nearestPointOnEdgeInPageSpace, pointInPageSpace); if (distance < snapDistance) { snap = "edge"; anchorInPageSpace = nearestPointOnEdgeInPageSpace; } } if (shouldSnapCenterAxis) { const snapDistance = calculateSnapDistance( editor, targetBoundsInTargetSpace, util.options.elbowArrowAxisSnapDistance ); const distanceFromXAxis = Vec.DistanceToLineSegment( handlesInPageSpace.left.far, handlesInPageSpace.right.far, pointInPageSpace ); const distanceFromYAxis = Vec.DistanceToLineSegment( handlesInPageSpace.top.far, handlesInPageSpace.bottom.far, pointInPageSpace ); const snapAxis = distanceFromXAxis < distanceFromYAxis && distanceFromXAxis < snapDistance ? "x" : distanceFromYAxis < snapDistance ? "y" : null; if (snapAxis) { const axis = ElbowArrowAxes[snapAxis]; const loDist2 = Vec.Dist2(handlesInPageSpace[axis.loEdge].far, pointInPageSpace); const hiDist2 = Vec.Dist2(handlesInPageSpace[axis.hiEdge].far, pointInPageSpace); const side = loDist2 < hiDist2 ? axis.loEdge : axis.hiEdge; if (handlesInPageSpace[side].isEnabled) { snap = "edge-point"; anchorInPageSpace = handlesInPageSpace[side].point; } } } if (shouldSnapEdgePoints) { const snapDistance = calculateSnapDistance( editor, targetBoundsInTargetSpace, util.options.elbowArrowPointSnapDistance ); let closestSide = null; let closestDistance = Infinity; for (const [side, handle] of objectMapEntries(handlesInPageSpace)) { if (!handle.isEnabled) continue; const distance = Vec.Dist(handle.point, pointInPageSpace); if (distance < snapDistance && distance < closestDistance) { closestDistance = distance; closestSide = side; } } if (closestSide) { snap = "edge-point"; anchorInPageSpace = handlesInPageSpace[closestSide].point; } } if (shouldSnapCenter) { const snapDistance = calculateSnapDistance( editor, targetBoundsInTargetSpace, arrowKind === "elbow" ? util.options.elbowArrowCenterSnapDistance : util.options.arcArrowCenterSnapDistance ); if (Vec.Dist(pointInTargetSpace, targetBoundsInTargetSpace.center) < snapDistance) { snap = "center"; anchorInPageSpace = targetCenterInPageSpace; } } const snapPointInTargetSpace = editor.getPointInShapeSpace(target, anchorInPageSpace); const normalizedAnchor = { x: invLerp( targetBoundsInTargetSpace.minX, targetBoundsInTargetSpace.maxX, snapPointInTargetSpace.x ), y: invLerp( targetBoundsInTargetSpace.minY, targetBoundsInTargetSpace.maxY, snapPointInTargetSpace.y ) }; const result = { target, arrowKind, handlesInPageSpace, centerInPageSpace: targetCenterInPageSpace, anchorInPageSpace, isExact, isPrecise: precise, snap, normalizedAnchor }; getArrowTargetAtom(editor).set(result); return result; } const targetFilterFallback = { type: "arrow" }; function calculateSnapDistance(editor, targetBoundsInTargetSpace, idealSnapDistance) { return clamp( Math.min(targetBoundsInTargetSpace.width, targetBoundsInTargetSpace.height) * 0.15, 4, idealSnapDistance ) / editor.getZoomLevel(); } export { clearArrowTargetState, getArrowTargetState, updateArrowTargetState }; //# sourceMappingURL=arrowTargetState.mjs.map