UNPKG

tldraw

Version:

A tiny little drawing editor.

222 lines (221 loc) • 7.52 kB
import { BindingUtil, Vec, approximately, arrowBindingMigrations, arrowBindingProps, getIndexAbove, getIndexBetween, intersectLineSegmentCircle } from "@tldraw/editor"; import { getArrowInfo } from "../../shapes/arrow/getArrowInfo.mjs"; import { getArrowBindings, 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 }, snap: "none" }; } // 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({ shapeBefore, shapeAfter, reason }) { if (reason !== "ancestry" && shapeBefore.parentId === shapeAfter.parentId && shapeBefore.index === shapeAfter.index) { return; } arrowDidUpdate(this.editor, shapeAfter); } // when the shape an arrow is bound to changes onAfterChangeToShape({ binding, shapeBefore, shapeAfter, reason }) { if (reason !== "ancestry" && shapeBefore.parentId === shapeAfter.parentId && shapeBefore.index === shapeAfter.index) { return; } 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 = getValidTerminalPoint( useHandle ? info.start.handle : info.start.point, arrow.props.start ); const endPoint = getValidTerminalPoint( useHandle ? info.end.handle : info.end.point, arrow.props.end ); 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.type === "arc") { const newStart = terminal === "start" ? startPoint : getValidTerminalPoint(info.start.handle, arrow.props.start); const newEnd = terminal === "end" ? endPoint : getValidTerminalPoint(info.end.handle, arrow.props.end); const newMidPoint = Vec.Med(newStart, newEnd); const arrowDirection = Vec.Sub(newStart, newEnd); if (approximately(Vec.Len2(arrowDirection), 0)) { editor.updateShape(update); if (unbind) { removeArrowBinding(editor, arrow, terminal); } return; } const lineSegment = arrowDirection.per().uni().mul(info.handleArc.radius * 2 * Math.sign(arrow.props.bend)); const targetPoint = Vec.Add(newMidPoint, lineSegment); if (!Vec.IsFinite(info.handleArc.center) || !Number.isFinite(info.handleArc.radius) || !Vec.IsFinite(targetPoint)) { editor.updateShape(update); if (unbind) { removeArrowBinding(editor, arrow, terminal); } return; } const intersections = intersectLineSegmentCircle( info.handleArc.center, targetPoint, info.handleArc.center, info.handleArc.radius ); if (intersections?.length) { const intersection = intersections.reduce( (closest, candidate) => Vec.Dist2(candidate, targetPoint) < Vec.Dist2(closest, targetPoint) ? candidate : closest ); const bend = Vec.Dist(newMidPoint, intersection) * Math.sign(arrow.props.bend); if (!approximately(bend, update.props.bend)) { update.props.bend = bend; } } } editor.updateShape(update); if (unbind) { removeArrowBinding(editor, arrow, terminal); } } function getValidTerminalPoint(point, fallback) { return Vec.From(Vec.IsFinite(point) ? point : fallback); } export { ArrowBindingUtil, updateArrowTerminal }; //# sourceMappingURL=ArrowBindingUtil.mjs.map