tldraw
Version:
A tiny little drawing editor.
178 lines (177 loc) • 6.52 kB
JavaScript
import {
intersectLineSegmentPolygon,
Mat,
pointInPolygon,
Vec
} from "@tldraw/editor";
import { createComputedCache } from "@tldraw/store";
const MIN_ARROW_BEND = 8;
const NORMALIZED_ANCHOR_EPSILON = 1e-3;
function clampNormalizedAnchor(anchor) {
const clamp = (v) => Math.max(NORMALIZED_ANCHOR_EPSILON, Math.min(1 - NORMALIZED_ANCHOR_EPSILON, v));
return { x: clamp(anchor.x), y: clamp(anchor.y) };
}
function getIsArrowStraight(shape) {
if (shape.props.kind !== "arc") return false;
return Math.abs(shape.props.bend) < MIN_ARROW_BEND * shape.props.scale;
}
function getBoundShapeInfoForTerminal(editor, arrow, terminalName) {
const binding = editor.getBindingsFromShape(arrow, "arrow").find((b) => b.props.terminal === terminalName);
if (!binding) return;
const boundShape = editor.getShape(binding.toId);
if (!boundShape) return;
const transform = editor.getShapePageTransform(boundShape);
const hasArrowhead = terminalName === "start" ? arrow.props.arrowheadStart !== "none" : arrow.props.arrowheadEnd !== "none";
const geometry = editor.getShapeGeometry(
boundShape,
hasArrowhead ? void 0 : { context: "@tldraw/arrow-without-arrowhead" }
);
return {
shape: boundShape,
transform,
isClosed: geometry.isClosed,
isExact: binding.props.isExact,
didIntersect: false,
geometry
};
}
function getArrowTerminalInArrowSpace(editor, arrowPageTransform, binding, forceImprecise) {
const boundShape = editor.getShape(binding.toId);
if (!boundShape) {
return new Vec(0, 0);
} else {
const { point, size } = editor.getShapeGeometry(boundShape).bounds;
const shouldUsePreciseAnchor = binding.props.isPrecise || forceImprecise;
const normalizedAnchor = shouldUsePreciseAnchor ? clampNormalizedAnchor(binding.props.normalizedAnchor) : { x: 0.5, y: 0.5 };
const shapePoint = Vec.Add(point, Vec.MulV(normalizedAnchor, size));
const pagePoint = Mat.applyToPoint(editor.getShapePageTransform(boundShape), shapePoint);
const arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pagePoint);
return arrowPoint;
}
}
const arrowBindingsCache = createComputedCache(
"arrow bindings",
(editor, arrow) => {
const bindings = editor.getBindingsFromShape(arrow.id, "arrow");
return {
start: bindings.find((b) => b.props.terminal === "start"),
end: bindings.find((b) => b.props.terminal === "end")
};
},
{
// we only look at the arrow IDs:
areRecordsEqual: (a, b) => a.id === b.id,
// the records should stay the same:
areResultsEqual: (a, b) => a.start === b.start && a.end === b.end
}
);
function getArrowBindings(editor, shape) {
return arrowBindingsCache.get(editor, shape.id);
}
function getArrowTerminalsInArrowSpace(editor, shape, bindings) {
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 };
}
function createOrUpdateArrowBinding(editor, arrow, target, props) {
const arrowId = typeof arrow === "string" ? arrow : arrow.id;
const targetId = typeof target === "string" ? target : target.id;
const existingMany = editor.getBindingsFromShape(arrowId, "arrow").filter((b) => b.props.terminal === props.terminal);
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
});
}
}
function removeArrowBinding(editor, arrow, terminal) {
const existing = editor.getBindingsFromShape(arrow, "arrow").filter((b) => b.props.terminal === terminal);
editor.deleteBindings(existing);
}
const MIN_ARROW_LENGTH = 10;
const BOUND_ARROW_OFFSET = 10;
const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10;
const STROKE_SIZES = {
s: 2,
m: 3.5,
l: 5,
xl: 10
};
function getBoundShapeRelationships(editor, startShapeId, endShapeId) {
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";
}
function clampArrowTerminalToMask(editor, point, terminalHandle, arrowPageTransform, targetShapeInfo) {
if (!targetShapeInfo) return;
const mask = editor.getShapeMask(targetShapeInfo.shape.id);
if (!mask) return;
const pagePoint = Mat.applyToPoint(arrowPageTransform, point);
if (pointInPolygon(pagePoint, mask)) return;
const pageAnchor = Mat.applyToPoint(arrowPageTransform, terminalHandle);
const direction = Vec.Sub(pageAnchor, pagePoint).uni();
const extendedAnchor = Vec.Add(pageAnchor, Vec.Mul(direction, 1));
const intersections = intersectLineSegmentPolygon(extendedAnchor, pagePoint, mask);
if (!intersections || intersections.length === 0) return;
let closest = intersections[0];
let closestDist = Vec.Dist2(closest, pagePoint);
for (let i = 1; i < intersections.length; i++) {
const dist = Vec.Dist2(intersections[i], pagePoint);
if (dist < closestDist) {
closest = intersections[i];
closestDist = dist;
}
}
const arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), closest);
point.setTo(arrowPoint);
}
export {
BOUND_ARROW_OFFSET,
MIN_ARROW_LENGTH,
STROKE_SIZES,
WAY_TOO_BIG_ARROW_BEND_FACTOR,
clampArrowTerminalToMask,
createOrUpdateArrowBinding,
getArrowBindings,
getArrowTerminalInArrowSpace,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
getBoundShapeRelationships,
getIsArrowStraight,
removeArrowBinding
};
//# sourceMappingURL=shared.mjs.map