UNPKG

tldraw

Version:

A tiny little drawing editor.

233 lines (232 loc) • 8.74 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var ArrowBindingUtil_exports = {}; __export(ArrowBindingUtil_exports, { ArrowBindingUtil: () => ArrowBindingUtil, updateArrowTerminal: () => updateArrowTerminal }); module.exports = __toCommonJS(ArrowBindingUtil_exports); var import_editor = require("@tldraw/editor"); var import_getArrowInfo = require("../../shapes/arrow/getArrowInfo"); var import_shared = require("../../shapes/arrow/shared"); class ArrowBindingUtil extends import_editor.BindingUtil { static type = "arrow"; static props = import_editor.arrowBindingProps; static migrations = import_editor.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 = (0, import_shared.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 = (0, import_editor.getIndexBetween)(highestSibling.index, higherSiblings[0].index); } else { finalIndex = (0, import_editor.getIndexAbove)(highestSibling.index); } if (finalIndex !== reparentedArrow.index) { editor.updateShapes([{ id: arrowId, type: "arrow", index: finalIndex }]); } } function arrowDidUpdate(editor, arrow) { const bindings = (0, import_shared.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 = (0, import_getArrowInfo.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 = import_editor.Vec.Med(newStart, newEnd); const arrowDirection = import_editor.Vec.Sub(newStart, newEnd); if ((0, import_editor.approximately)(import_editor.Vec.Len2(arrowDirection), 0)) { editor.updateShape(update); if (unbind) { (0, import_shared.removeArrowBinding)(editor, arrow, terminal); } return; } const lineSegment = arrowDirection.per().uni().mul(info.handleArc.radius * 2 * Math.sign(arrow.props.bend)); const targetPoint = import_editor.Vec.Add(newMidPoint, lineSegment); if (!import_editor.Vec.IsFinite(info.handleArc.center) || !Number.isFinite(info.handleArc.radius) || !import_editor.Vec.IsFinite(targetPoint)) { editor.updateShape(update); if (unbind) { (0, import_shared.removeArrowBinding)(editor, arrow, terminal); } return; } const intersections = (0, import_editor.intersectLineSegmentCircle)( info.handleArc.center, targetPoint, info.handleArc.center, info.handleArc.radius ); if (intersections?.length) { const intersection = intersections.reduce( (closest, candidate) => import_editor.Vec.Dist2(candidate, targetPoint) < import_editor.Vec.Dist2(closest, targetPoint) ? candidate : closest ); const bend = import_editor.Vec.Dist(newMidPoint, intersection) * Math.sign(arrow.props.bend); if (!(0, import_editor.approximately)(bend, update.props.bend)) { update.props.bend = bend; } } } editor.updateShape(update); if (unbind) { (0, import_shared.removeArrowBinding)(editor, arrow, terminal); } } function getValidTerminalPoint(point, fallback) { return import_editor.Vec.From(import_editor.Vec.IsFinite(point) ? point : fallback); } //# sourceMappingURL=ArrowBindingUtil.js.map