UNPKG

tldraw

Version:

A tiny little drawing editor.

178 lines (177 loc) • 6.52 kB
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