tldraw
Version:
A tiny little drawing editor.
252 lines (251 loc) • 8.74 kB
JavaScript
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