UNPKG

tldraw

Version:

A tiny little drawing editor.

8 lines (7 loc) • 14 kB
{ "version": 3, "sources": ["../../../../src/lib/shapes/arrow/shared.ts"], "sourcesContent": ["import {\n\tEditor,\n\tGeometry2d,\n\tintersectLineSegmentPolygon,\n\tMat,\n\tMatModel,\n\tpointInPolygon,\n\tTLArrowBinding,\n\tTLArrowBindingProps,\n\tTLArrowShape,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n} from '@tldraw/editor'\nimport { createComputedCache } from '@tldraw/store'\n\nconst MIN_ARROW_BEND = 8\n// Keep anchors off exact edges/corners to avoid degenerate arrow intersections.\nconst NORMALIZED_ANCHOR_EPSILON = 1e-3\n\nfunction clampNormalizedAnchor(anchor: { x: number; y: number }) {\n\tconst clamp = (v: number) =>\n\t\tMath.max(NORMALIZED_ANCHOR_EPSILON, Math.min(1 - NORMALIZED_ANCHOR_EPSILON, v))\n\treturn { x: clamp(anchor.x), y: clamp(anchor.y) }\n}\n\nexport function getIsArrowStraight(shape: TLArrowShape) {\n\tif (shape.props.kind !== 'arc') return false\n\treturn Math.abs(shape.props.bend) < MIN_ARROW_BEND * shape.props.scale // snap to +-8px\n}\n\nexport interface BoundShapeInfo<T extends TLShape = TLShape> {\n\tshape: T\n\tdidIntersect: boolean\n\tisExact: boolean\n\tisClosed: boolean\n\ttransform: Mat\n\tgeometry: Geometry2d\n}\n\nexport function getBoundShapeInfoForTerminal(\n\teditor: Editor,\n\tarrow: TLArrowShape,\n\tterminalName: 'start' | 'end'\n): BoundShapeInfo | undefined {\n\tconst binding = editor\n\t\t.getBindingsFromShape(arrow, 'arrow')\n\t\t.find((b) => b.props.terminal === terminalName)\n\tif (!binding) return\n\n\tconst boundShape = editor.getShape(binding.toId)!\n\tif (!boundShape) return\n\tconst transform = editor.getShapePageTransform(boundShape)!\n\tconst hasArrowhead =\n\t\tterminalName === 'start'\n\t\t\t? arrow.props.arrowheadStart !== 'none'\n\t\t\t: arrow.props.arrowheadEnd !== 'none'\n\tconst geometry = editor.getShapeGeometry(\n\t\tboundShape,\n\t\thasArrowhead ? undefined : { context: '@tldraw/arrow-without-arrowhead' }\n\t)\n\n\treturn {\n\t\tshape: boundShape,\n\t\ttransform,\n\t\tisClosed: geometry.isClosed,\n\t\tisExact: binding.props.isExact,\n\t\tdidIntersect: false,\n\t\tgeometry,\n\t}\n}\n\nexport function getArrowTerminalInArrowSpace(\n\teditor: Editor,\n\tarrowPageTransform: Mat,\n\tbinding: TLArrowBinding,\n\tforceImprecise: boolean\n) {\n\tconst boundShape = editor.getShape(binding.toId)\n\n\tif (!boundShape) {\n\t\t// this can happen in multiplayer contexts where the shape is being deleted\n\t\treturn new Vec(0, 0)\n\t} else {\n\t\t// Find the actual local point of the normalized terminal on\n\t\t// the bound shape and transform it to page space, then transform\n\t\t// it to arrow space\n\t\tconst { point, size } = editor.getShapeGeometry(boundShape).bounds\n\t\t// If the parent is the bound shape, then it's always treated as precise.\n\t\tconst shouldUsePreciseAnchor = binding.props.isPrecise || forceImprecise\n\t\tconst normalizedAnchor = shouldUsePreciseAnchor\n\t\t\t? clampNormalizedAnchor(binding.props.normalizedAnchor)\n\t\t\t: { x: 0.5, y: 0.5 }\n\n\t\tconst shapePoint = Vec.Add(point, Vec.MulV(normalizedAnchor, size))\n\t\tconst pagePoint = Mat.applyToPoint(editor.getShapePageTransform(boundShape)!, shapePoint)\n\t\tconst arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pagePoint)\n\t\treturn arrowPoint\n\t}\n}\n\n/** @public */\nexport interface TLArrowBindings {\n\tstart: TLArrowBinding | undefined\n\tend: TLArrowBinding | undefined\n}\n\nconst arrowBindingsCache = createComputedCache(\n\t'arrow bindings',\n\t(editor: Editor, arrow: TLArrowShape) => {\n\t\tconst bindings = editor.getBindingsFromShape(arrow.id, 'arrow')\n\t\treturn {\n\t\t\tstart: bindings.find((b) => b.props.terminal === 'start'),\n\t\t\tend: bindings.find((b) => b.props.terminal === 'end'),\n\t\t}\n\t},\n\t{\n\t\t// we only look at the arrow IDs:\n\t\tareRecordsEqual: (a, b) => a.id === b.id,\n\t\t// the records should stay the same:\n\t\tareResultsEqual: (a, b) => a.start === b.start && a.end === b.end,\n\t}\n)\n\n/** @public */\nexport function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings {\n\treturn arrowBindingsCache.get(editor, shape.id)!\n}\n\n/** @public */\nexport function getArrowTerminalsInArrowSpace(\n\teditor: Editor,\n\tshape: TLArrowShape,\n\tbindings: TLArrowBindings\n) {\n\tconst arrowPageTransform = editor.getShapePageTransform(shape)!\n\n\tconst boundShapeRelationships = getBoundShapeRelationships(\n\t\teditor,\n\t\tbindings.start?.toId,\n\t\tbindings.end?.toId\n\t)\n\n\tconst start = bindings.start\n\t\t? getArrowTerminalInArrowSpace(\n\t\t\t\teditor,\n\t\t\t\tarrowPageTransform,\n\t\t\t\tbindings.start,\n\t\t\t\tboundShapeRelationships === 'double-bound' ||\n\t\t\t\t\tboundShapeRelationships === 'start-contains-end'\n\t\t\t)\n\t\t: Vec.From(shape.props.start)\n\n\tconst end = bindings.end\n\t\t? getArrowTerminalInArrowSpace(\n\t\t\t\teditor,\n\t\t\t\tarrowPageTransform,\n\t\t\t\tbindings.end,\n\t\t\t\tboundShapeRelationships === 'double-bound' ||\n\t\t\t\t\tboundShapeRelationships === 'end-contains-start'\n\t\t\t)\n\t\t: Vec.From(shape.props.end)\n\n\treturn { start, end }\n}\n\n/**\n * Create or update the arrow binding for a particular arrow terminal. Will clear up if needed.\n * @internal\n */\nexport function createOrUpdateArrowBinding(\n\teditor: Editor,\n\tarrow: TLArrowShape | TLShapeId,\n\ttarget: TLShape | TLShapeId,\n\tprops: TLArrowBindingProps\n) {\n\tconst arrowId = typeof arrow === 'string' ? arrow : arrow.id\n\tconst targetId = typeof target === 'string' ? target : target.id\n\n\tconst existingMany = editor\n\t\t.getBindingsFromShape(arrowId, 'arrow')\n\t\t.filter((b) => b.props.terminal === props.terminal)\n\n\t// if we've somehow ended up with too many bindings, delete the extras\n\tif (existingMany.length > 1) {\n\t\teditor.deleteBindings(existingMany.slice(1))\n\t}\n\n\tconst existing = existingMany[0]\n\tif (existing) {\n\t\teditor.updateBinding({\n\t\t\t...existing,\n\t\t\ttoId: targetId,\n\t\t\tprops,\n\t\t})\n\t} else {\n\t\teditor.createBinding({\n\t\t\ttype: 'arrow',\n\t\t\tfromId: arrowId,\n\t\t\ttoId: targetId,\n\t\t\tprops,\n\t\t})\n\t}\n}\n\n/**\n * Remove any arrow bindings for a particular terminal.\n * @internal\n */\nexport function removeArrowBinding(editor: Editor, arrow: TLArrowShape, terminal: 'start' | 'end') {\n\tconst existing = editor\n\t\t.getBindingsFromShape(arrow, 'arrow')\n\t\t.filter((b) => b.props.terminal === terminal)\n\n\teditor.deleteBindings(existing)\n}\n\n/** @internal */\nexport const MIN_ARROW_LENGTH = 10\n/** @internal */\nexport const BOUND_ARROW_OFFSET = 10\n/** @internal */\nexport const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10\n\n/** @public */\nexport const STROKE_SIZES: Record<string, number> = {\n\ts: 2,\n\tm: 3.5,\n\tl: 5,\n\txl: 10,\n}\n\n/**\n * Get the relationships for an arrow that has two bound shape terminals.\n * If the arrow has only one bound shape, then it is always \"safe\" to apply\n * standard offsets and precision behavior. If the shape is bound to the same\n * shape on both ends, then that is an exception. If one of the shape's\n * terminals is bound to a shape that contains / is contained by the shape that\n * is bound to the other terminal, then that is also an exception.\n *\n * @param editor - the editor instance\n * @param startShapeId - the bound shape from the arrow's start\n * @param endShapeId - the bound shape from the arrow's end\n *\n * @internal */\nexport function getBoundShapeRelationships(\n\teditor: Editor,\n\tstartShapeId?: TLShapeId,\n\tendShapeId?: TLShapeId\n) {\n\tif (!startShapeId || !endShapeId) return 'safe'\n\tif (startShapeId === endShapeId) return 'double-bound'\n\tconst startBounds = editor.getShapePageBounds(startShapeId)\n\tconst endBounds = editor.getShapePageBounds(endShapeId)\n\tif (startBounds && endBounds) {\n\t\tif (startBounds.contains(endBounds)) return 'start-contains-end'\n\t\tif (endBounds.contains(startBounds)) return 'end-contains-start'\n\t}\n\treturn 'safe'\n}\n\n/**\n * If the arrow terminal point falls outside the bound shape's mask (e.g. when a shape\n * extends beyond a frame boundary and is clipped), clamp the terminal to the mask boundary.\n * Uses the binding anchor point (inside the shape/frame) as the ray origin, since the\n * arrow endpoint may be entirely outside the mask.\n *\n * @internal\n */\nexport function clampArrowTerminalToMask(\n\teditor: Editor,\n\tpoint: Vec,\n\tterminalHandle: Vec,\n\tarrowPageTransform: MatModel,\n\ttargetShapeInfo?: BoundShapeInfo\n) {\n\tif (!targetShapeInfo) return\n\n\tconst mask = editor.getShapeMask(targetShapeInfo.shape.id)\n\tif (!mask) return\n\n\tconst pagePoint = Mat.applyToPoint(arrowPageTransform, point)\n\n\tif (pointInPolygon(pagePoint, mask)) return\n\n\t// The point is outside the mask (clipped). Cast a ray from the binding\n\t// anchor (which is inside the shape, and typically inside the frame) through\n\t// the intersection point on the shape boundary to find where it crosses the mask.\n\t// We extend the line slightly past the anchor in case the anchor sits exactly\n\t// on the mask boundary.\n\tconst pageAnchor = Mat.applyToPoint(arrowPageTransform, terminalHandle)\n\tconst direction = Vec.Sub(pageAnchor, pagePoint).uni()\n\tconst extendedAnchor = Vec.Add(pageAnchor, Vec.Mul(direction, 1))\n\tconst intersections = intersectLineSegmentPolygon(extendedAnchor, pagePoint, mask)\n\tif (!intersections || intersections.length === 0) return\n\n\t// Pick the intersection closest to the original point (nearest frame edge to the shape)\n\tlet closest = intersections[0]\n\tlet closestDist = Vec.Dist2(closest, pagePoint)\n\tfor (let i = 1; i < intersections.length; i++) {\n\t\tconst dist = Vec.Dist2(intersections[i], pagePoint)\n\t\tif (dist < closestDist) {\n\t\t\tclosest = intersections[i]\n\t\t\tclosestDist = dist\n\t\t}\n\t}\n\n\tconst arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), closest)\n\tpoint.setTo(arrowPoint)\n}\n"], "mappings": "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", "names": [] }