UNPKG

tldraw

Version:

A tiny little drawing editor.

179 lines (178 loc) • 6.08 kB
import { BindingUtil, Vec, approximately, arrowBindingMigrations, arrowBindingProps, assert, getIndexAbove, getIndexBetween, intersectLineSegmentCircle } from "@tldraw/editor"; import { getArrowBindings, getArrowInfo, removeArrowBinding } from "../../shapes/arrow/shared.mjs"; class ArrowBindingUtil extends BindingUtil { static type = "arrow"; static props = arrowBindingProps; static migrations = arrowBindingMigrations; getDefaultProps() { return { isPrecise: false, isExact: false, normalizedAnchor: { x: 0.5, y: 0.5 } }; } // when the binding itself changes onAfterCreate({ binding }) { const arrow = this.editor.getShape(binding.fromId); if (!arrow) return; arrowDidUpdate(this.editor, arrow); } // when the binding itself changes onAfterChange({ bindingAfter }) { const arrow = this.editor.getShape(bindingAfter.fromId); if (!arrow) return; arrowDidUpdate(this.editor, arrow); } // when the arrow itself changes onAfterChangeFromShape({ shapeAfter }) { arrowDidUpdate(this.editor, shapeAfter); } // when the shape an arrow is bound to changes onAfterChangeToShape({ binding }) { reparentArrow(this.editor, binding.fromId); } // when the arrow is isolated we need to update it's x,y positions onBeforeIsolateFromShape({ binding }) { const arrow = this.editor.getShape(binding.fromId); if (!arrow) return; updateArrowTerminal({ editor: this.editor, arrow, terminal: binding.props.terminal }); } } function reparentArrow(editor, arrowId) { const arrow = editor.getShape(arrowId); if (!arrow) return; const bindings = getArrowBindings(editor, arrow); const { start, end } = bindings; const startShape = start ? editor.getShape(start.toId) : void 0; const endShape = end ? editor.getShape(end.toId) : void 0; const parentPageId = editor.getAncestorPageId(arrow); if (!parentPageId) return; let nextParentId; if (startShape && endShape) { nextParentId = editor.findCommonAncestor([startShape, endShape]) ?? parentPageId; } else if (startShape || endShape) { const bindingParentId = (startShape || endShape)?.parentId; if (bindingParentId && bindingParentId === arrow.parentId) { nextParentId = arrow.parentId; } else { nextParentId = parentPageId; } } else { return; } if (nextParentId && nextParentId !== arrow.parentId) { editor.reparentShapes([arrowId], nextParentId); } const reparentedArrow = editor.getShape(arrowId); if (!reparentedArrow) throw Error("no reparented arrow"); const startSibling = editor.getShapeNearestSibling(reparentedArrow, startShape); const endSibling = editor.getShapeNearestSibling(reparentedArrow, endShape); let highestSibling; if (startSibling && endSibling) { highestSibling = startSibling.index > endSibling.index ? startSibling : endSibling; } else if (startSibling && !endSibling) { highestSibling = startSibling; } else if (endSibling && !startSibling) { highestSibling = endSibling; } else { return; } let finalIndex; const higherSiblings = editor.getSortedChildIdsForParent(highestSibling.parentId).map((id) => editor.getShape(id)).filter((sibling) => sibling.index > highestSibling.index); if (higherSiblings.length) { const nextHighestNonArrowSibling = higherSiblings.find((sibling) => sibling.type !== "arrow"); if ( // ...then, if we're above the last shape we want to be above... reparentedArrow.index > highestSibling.index && // ...but below the next non-arrow sibling... (!nextHighestNonArrowSibling || reparentedArrow.index < nextHighestNonArrowSibling.index) ) { return; } finalIndex = getIndexBetween(highestSibling.index, higherSiblings[0].index); } else { finalIndex = getIndexAbove(highestSibling.index); } if (finalIndex !== reparentedArrow.index) { editor.updateShapes([{ id: arrowId, type: "arrow", index: finalIndex }]); } } function arrowDidUpdate(editor, arrow) { const bindings = getArrowBindings(editor, arrow); for (const handle of ["start", "end"]) { const binding = bindings[handle]; if (!binding) continue; const boundShape = editor.getShape(binding.toId); const isShapeInSamePageAsArrow = editor.getAncestorPageId(arrow) === editor.getAncestorPageId(boundShape); if (!boundShape || !isShapeInSamePageAsArrow) { updateArrowTerminal({ editor, arrow, terminal: handle, unbind: true }); } } reparentArrow(editor, arrow.id); } function updateArrowTerminal({ editor, arrow, terminal, unbind = false, useHandle = false }) { const info = getArrowInfo(editor, arrow); if (!info) { throw new Error("expected arrow info"); } const startPoint = useHandle ? info.start.handle : info.start.point; const endPoint = useHandle ? info.end.handle : info.end.point; const point = terminal === "start" ? startPoint : endPoint; const update = { id: arrow.id, type: "arrow", props: { [terminal]: { x: point.x, y: point.y }, bend: arrow.props.bend } }; if (!info.isStraight) { const newStart = terminal === "start" ? startPoint : info.start.handle; const newEnd = terminal === "end" ? endPoint : info.end.handle; const newMidPoint = Vec.Med(newStart, newEnd); const lineSegment = Vec.Sub(newStart, newEnd).per().uni().mul(info.handleArc.radius * 2 * Math.sign(arrow.props.bend)); const intersections = intersectLineSegmentCircle( info.handleArc.center, Vec.Add(newMidPoint, lineSegment), info.handleArc.center, info.handleArc.radius ); assert(intersections?.length === 1); const bend = Vec.Dist(newMidPoint, intersections[0]) * Math.sign(arrow.props.bend); if (!approximately(bend, update.props.bend)) { update.props.bend = bend; } } editor.updateShape(update); if (unbind) { removeArrowBinding(editor, arrow, terminal); } } export { ArrowBindingUtil, updateArrowTerminal }; //# sourceMappingURL=ArrowBindingUtil.mjs.map