UNPKG

tldraw

Version:

A tiny little drawing editor.

1,043 lines (1,042 loc) • 43.4 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 ArrowShapeUtil_exports = {}; __export(ArrowShapeUtil_exports, { ArrowShapeUtil: () => ArrowShapeUtil, getArrowLength: () => getArrowLength }); module.exports = __toCommonJS(ArrowShapeUtil_exports); var import_jsx_runtime = require("react/jsx-runtime"); var import_editor = require("@tldraw/editor"); var import_react = require("react"); var import_ArrowBindingUtil = require("../../bindings/arrow/ArrowBindingUtil"); var import_richText = require("../../utils/text/richText"); var import_PathBuilder = require("../shared/PathBuilder"); var import_RichTextLabel = require("../shared/RichTextLabel"); var import_ShapeFill = require("../shared/ShapeFill"); var import_default_shape_constants = require("../shared/default-shape-constants"); var import_defaultStyleDefs = require("../shared/defaultStyleDefs"); var import_useDefaultColorTheme = require("../shared/useDefaultColorTheme"); var import_useEfficientZoomThreshold = require("../shared/useEfficientZoomThreshold"); var import_ArrowPath = require("./ArrowPath"); var import_arrowLabel = require("./arrowLabel"); var import_arrowTargetState = require("./arrowTargetState"); var import_arrowheads = require("./arrowheads"); var import_ElbowArrowDebug = require("./elbow/ElbowArrowDebug"); var import_definitions = require("./elbow/definitions"); var import_elbowArrowSnapLines = require("./elbow/elbowArrowSnapLines"); var import_getArrowInfo = require("./getArrowInfo"); var import_shared = require("./shared"); const ArrowHandles = { Start: "start", Middle: "middle", End: "end" }; class ArrowShapeUtil extends import_editor.ShapeUtil { static type = "arrow"; static props = import_editor.arrowShapeProps; static migrations = import_editor.arrowShapeMigrations; options = { expandElbowLegLength: { s: 28, m: 36, l: 44, xl: 66 }, minElbowLegLength: { s: import_default_shape_constants.STROKE_SIZES.s * 3, m: import_default_shape_constants.STROKE_SIZES.m * 3, l: import_default_shape_constants.STROKE_SIZES.l * 3, xl: import_default_shape_constants.STROKE_SIZES.xl * 3 }, minElbowHandleDistance: 16, arcArrowCenterSnapDistance: 16, elbowArrowCenterSnapDistance: 24, elbowArrowEdgeSnapDistance: 20, elbowArrowPointSnapDistance: 24, elbowArrowAxisSnapDistance: 16, labelCenterSnapDistance: 10, elbowMidpointSnapDistance: 10, elbowMinSegmentLengthToShowMidpointHandle: 20, hoverPreciseTimeout: 600, pointingPreciseTimeout: 320, shouldBeExact(editor) { return editor.inputs.getAltKey(); }, shouldIgnoreTargets(editor) { return editor.inputs.getCtrlKey(); }, showTextOutline: true }; canEdit() { return true; } canBind({ toShape }) { return toShape.type !== "arrow"; } canSnap() { return false; } hideResizeHandles() { return true; } hideRotateHandle() { return true; } hideSelectionBoundsBg() { return true; } hideSelectionBoundsFg() { return true; } hideInMinimap() { return true; } canBeLaidOut(shape, info) { if (info.type === "flip") { const bindings = (0, import_shared.getArrowBindings)(this.editor, shape); const { start, end } = bindings; const { shapes = [] } = info; if (start && !shapes.find((s) => s.id === start.toId)) return false; if (end && !shapes.find((s) => s.id === end.toId)) return false; } return true; } getFontFaces(shape) { if ((0, import_richText.isEmptyRichText)(shape.props.richText)) return import_editor.EMPTY_ARRAY; return (0, import_editor.getFontsFromRichText)(this.editor, shape.props.richText, { family: `tldraw_${shape.props.font}`, weight: "normal", style: "normal" }); } getDefaultProps() { return { kind: "arc", elbowMidPoint: 0.5, dash: "draw", size: "m", fill: "none", color: "black", labelColor: "black", bend: 0, start: { x: 0, y: 0 }, end: { x: 2, y: 0 }, arrowheadStart: "none", arrowheadEnd: "arrow", richText: (0, import_editor.toRichText)(""), labelPosition: 0.5, font: "draw", scale: 1 }; } getGeometry(shape) { const isEditing = this.editor.getEditingShapeId() === shape.id; const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); const debugGeom = []; const bodyGeom = info.type === "straight" ? new import_editor.Edge2d({ start: import_editor.Vec.From(info.start.point), end: import_editor.Vec.From(info.end.point) }) : info.type === "arc" ? new import_editor.Arc2d({ center: import_editor.Vec.Cast(info.handleArc.center), start: import_editor.Vec.Cast(info.start.point), end: import_editor.Vec.Cast(info.end.point), sweepFlag: info.bodyArc.sweepFlag, largeArcFlag: info.bodyArc.largeArcFlag }) : new import_editor.Polyline2d({ points: info.route.points }); let labelGeom; if (info.isValid && (isEditing || !(0, import_richText.isEmptyRichText)(shape.props.richText))) { const labelPosition = (0, import_arrowLabel.getArrowLabelPosition)(this.editor, shape, isEditing); if (import_editor.debugFlags.debugGeometry.get()) { debugGeom.push(...labelPosition.debugGeom); } labelGeom = new import_editor.Rectangle2d({ x: labelPosition.box.x, y: labelPosition.box.y, width: labelPosition.box.w, height: labelPosition.box.h, isFilled: true, isLabel: true }); } return new import_editor.Group2d({ children: [...labelGeom ? [bodyGeom, labelGeom] : [bodyGeom], ...debugGeom] }); } getHandles(shape) { const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); const handles = [ { id: ArrowHandles.Start, type: "vertex", index: "a1", x: info.start.handle.x, y: info.start.handle.y }, { id: ArrowHandles.End, type: "vertex", index: "a3", x: info.end.handle.x, y: info.end.handle.y } ]; if (shape.props.kind === "arc" && (info.type === "straight" || info.type === "arc")) { handles.push({ id: ArrowHandles.Middle, type: "virtual", index: "a2", x: info.middle.x, y: info.middle.y }); } if (shape.props.kind === "elbow" && info.type === "elbow" && info.route.midpointHandle) { const shapePageTransform = this.editor.getShapePageTransform(shape.id); const segmentStart = shapePageTransform.applyToPoint(info.route.midpointHandle.segmentStart); const segmentEnd = shapePageTransform.applyToPoint(info.route.midpointHandle.segmentEnd); const segmentLength = import_editor.Vec.Dist(segmentStart, segmentEnd) * this.editor.getEfficientZoomLevel(); if (segmentLength > this.options.elbowMinSegmentLengthToShowMidpointHandle) { handles.push({ id: ArrowHandles.Middle, type: "vertex", index: "a2", x: info.route.midpointHandle.point.x, y: info.route.midpointHandle.point.y }); } } return handles; } getText(shape) { return (0, import_richText.renderPlaintextFromRichText)(this.editor, shape.props.richText); } onHandleDrag(shape, info) { const handleId = info.handle.id; switch (handleId) { case ArrowHandles.Middle: switch (shape.props.kind) { case "arc": return this.onArcMidpointHandleDrag(shape, info); case "elbow": return this.onElbowMidpointHandleDrag(shape, info); default: (0, import_editor.exhaustiveSwitchError)(shape.props.kind); } case ArrowHandles.Start: case ArrowHandles.End: return this.onTerminalHandleDrag(shape, info, handleId); default: (0, import_editor.exhaustiveSwitchError)(handleId); } } onArcMidpointHandleDrag(shape, { handle }) { const bindings = (0, import_shared.getArrowBindings)(this.editor, shape); const { start, end } = (0, import_shared.getArrowTerminalsInArrowSpace)(this.editor, shape, bindings); const delta = import_editor.Vec.Sub(end, start); const v = import_editor.Vec.Per(delta); const med = import_editor.Vec.Med(end, start); const A = import_editor.Vec.Sub(med, v); const B = import_editor.Vec.Add(med, v); const point = import_editor.Vec.NearestPointOnLineSegment(A, B, handle, false); let bend = import_editor.Vec.Dist(point, med); if (import_editor.Vec.Clockwise(point, end, med)) bend *= -1; return { id: shape.id, type: shape.type, props: { bend } }; } onElbowMidpointHandleDrag(shape, { handle }) { const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); if (info?.type !== "elbow") return; const shapeToPageTransform = this.editor.getShapePageTransform(shape.id); const handlePagePoint = shapeToPageTransform.applyToPoint(handle); const axisName = info.route.midpointHandle?.axis; if (!axisName) return; const axis = import_definitions.ElbowArrowAxes[axisName]; const midRange = info.elbow[axis.midRange]; if (!midRange) return; let angle = import_editor.Vec.Angle( shapeToPageTransform.applyToPoint(axis.v(0, 0)), shapeToPageTransform.applyToPoint(axis.v(0, 1)) ); if (angle < 0) angle += Math.PI; const handlePoint = (0, import_elbowArrowSnapLines.perpDistanceToLineAngle)(handlePagePoint, angle); const loPoint = (0, import_elbowArrowSnapLines.perpDistanceToLineAngle)( shapeToPageTransform.applyToPoint(axis.v(midRange.lo, 0)), angle ); const hiPoint = (0, import_elbowArrowSnapLines.perpDistanceToLineAngle)( shapeToPageTransform.applyToPoint(axis.v(midRange.hi, 0)), angle ); const maxSnapDistance = this.options.elbowMidpointSnapDistance / this.editor.getEfficientZoomLevel(); const midPoint = (0, import_elbowArrowSnapLines.perpDistanceToLineAngle)( shapeToPageTransform.applyToPoint(axis.v((0, import_editor.lerp)(midRange.lo, midRange.hi, 0.5), 0)), angle ); let snapPoint = midPoint; let snapDistance = Math.abs(midPoint - handlePoint); for (const [snapAngle, snapLines] of (0, import_elbowArrowSnapLines.getElbowArrowSnapLines)(this.editor)) { const { isParallel, isFlippedParallel } = anglesAreApproximatelyParallel(angle, snapAngle); if (isParallel || isFlippedParallel) { for (const snapLine of snapLines) { const doesShareStartIntersection = snapLine.startBoundShapeId && (snapLine.startBoundShapeId === info.bindings.start?.toId || snapLine.startBoundShapeId === info.bindings.end?.toId); const doesShareEndIntersection = snapLine.endBoundShapeId && (snapLine.endBoundShapeId === info.bindings.start?.toId || snapLine.endBoundShapeId === info.bindings.end?.toId); if (!doesShareStartIntersection && !doesShareEndIntersection) continue; const point = isFlippedParallel ? -snapLine.perpDistance : snapLine.perpDistance; const distance = Math.abs(point - handlePoint); if (distance < snapDistance) { snapPoint = point; snapDistance = distance; } } } } if (snapDistance > maxSnapDistance) { snapPoint = handlePoint; } const newMid = (0, import_editor.clamp)((0, import_editor.invLerp)(loPoint, hiPoint, snapPoint), 0, 1); return { id: shape.id, type: shape.type, props: { elbowMidPoint: newMid } }; } onTerminalHandleDrag(shape, { handle, isPrecise }, handleId) { const bindings = (0, import_shared.getArrowBindings)(this.editor, shape); const update = { id: shape.id, type: "arrow", props: {} }; const currentBinding = bindings[handleId]; const oppositeHandleId = handleId === ArrowHandles.Start ? ArrowHandles.End : ArrowHandles.Start; const oppositeBinding = bindings[oppositeHandleId]; const targetInfo = (0, import_arrowTargetState.updateArrowTargetState)({ editor: this.editor, pointInPageSpace: this.editor.getShapePageTransform(shape.id).applyToPoint(handle), arrow: shape, isPrecise, currentBinding, oppositeBinding }); if (!targetInfo) { (0, import_shared.removeArrowBinding)(this.editor, shape, handleId); const newPoint = (0, import_editor.maybeSnapToGrid)(new import_editor.Vec(handle.x, handle.y), this.editor); update.props[handleId] = { x: newPoint.x, y: newPoint.y }; return update; } const bindingProps = { terminal: handleId, normalizedAnchor: targetInfo.normalizedAnchor, isPrecise: targetInfo.isPrecise, isExact: targetInfo.isExact, snap: targetInfo.snap }; (0, import_shared.createOrUpdateArrowBinding)(this.editor, shape, targetInfo.target.id, bindingProps); const newBindings = (0, import_shared.getArrowBindings)(this.editor, shape); if (newBindings.start && newBindings.end && newBindings.start.toId === newBindings.end.toId) { if (import_editor.Vec.Equals(newBindings.start.props.normalizedAnchor, newBindings.end.props.normalizedAnchor)) { (0, import_shared.createOrUpdateArrowBinding)(this.editor, shape, newBindings.end.toId, { ...newBindings.end.props, normalizedAnchor: { x: newBindings.end.props.normalizedAnchor.x + 0.05, y: newBindings.end.props.normalizedAnchor.y } }); } } return update; } onTranslateStart(shape) { const bindings = (0, import_shared.getArrowBindings)(this.editor, shape); if (shape.props.kind === "elbow" && this.editor.getOnlySelectedShapeId() === shape.id) { const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); if (!info) return; const update = { id: shape.id, type: "arrow", props: {} }; if (bindings.start) { update.props.start = { x: info.start.point.x, y: info.start.point.y }; (0, import_shared.removeArrowBinding)(this.editor, shape, "start"); } if (bindings.end) { update.props.end = { x: info.end.point.x, y: info.end.point.y }; (0, import_shared.removeArrowBinding)(this.editor, shape, "end"); } return update; } const terminalsInArrowSpace = (0, import_shared.getArrowTerminalsInArrowSpace)(this.editor, shape, bindings); const shapePageTransform = this.editor.getShapePageTransform(shape.id); const selectedShapeIds = this.editor.getSelectedShapeIds(); if (bindings.start && (selectedShapeIds.includes(bindings.start.toId) || this.editor.isAncestorSelected(bindings.start.toId)) || bindings.end && (selectedShapeIds.includes(bindings.end.toId) || this.editor.isAncestorSelected(bindings.end.toId))) { return; } shapeAtTranslationStart.set(shape, { pagePosition: shapePageTransform.applyToPoint(shape), terminalBindings: (0, import_editor.mapObjectMapValues)(terminalsInArrowSpace, (terminalName, point) => { const binding = bindings[terminalName]; if (!binding) return null; return { binding, shapePosition: point, pagePosition: shapePageTransform.applyToPoint(point) }; }) }); if (bindings.start) { (0, import_ArrowBindingUtil.updateArrowTerminal)({ editor: this.editor, arrow: shape, terminal: "start", useHandle: true }); shape = this.editor.getShape(shape.id); } if (bindings.end) { (0, import_ArrowBindingUtil.updateArrowTerminal)({ editor: this.editor, arrow: shape, terminal: "end", useHandle: true }); } for (const handleName of [ArrowHandles.Start, ArrowHandles.End]) { const binding = bindings[handleName]; if (!binding) continue; this.editor.updateBinding({ ...binding, props: { ...binding.props, isPrecise: true } }); } return; } onTranslate(initialShape, shape) { const atTranslationStart = shapeAtTranslationStart.get(initialShape); if (!atTranslationStart) return; const shapePageTransform = this.editor.getShapePageTransform(shape.id); const pageDelta = import_editor.Vec.Sub( shapePageTransform.applyToPoint(shape), atTranslationStart.pagePosition ); for (const terminalBinding of Object.values(atTranslationStart.terminalBindings)) { if (!terminalBinding) continue; const newPagePoint = import_editor.Vec.Add(terminalBinding.pagePosition, import_editor.Vec.Mul(pageDelta, 0.5)); const newTarget = this.editor.getShapeAtPoint(newPagePoint, { hitInside: true, hitFrameInside: true, margin: 0, filter: (targetShape) => { return !targetShape.isLocked && this.editor.canBindShapes({ fromShape: shape, toShape: targetShape, binding: "arrow" }); } }); if (newTarget?.id === terminalBinding.binding.toId) { const targetBounds = import_editor.Box.ZeroFix(this.editor.getShapeGeometry(newTarget).bounds); const pointInTargetSpace = this.editor.getPointInShapeSpace(newTarget, newPagePoint); const normalizedAnchor = { x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width, y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height }; (0, import_shared.createOrUpdateArrowBinding)(this.editor, shape, newTarget.id, { ...terminalBinding.binding.props, normalizedAnchor, isPrecise: true }); } else { (0, import_shared.removeArrowBinding)(this.editor, shape, terminalBinding.binding.props.terminal); } } } _resizeInitialBindings = new import_editor.WeakCache(); onResize(shape, info) { const { scaleX, scaleY } = info; const bindings = this._resizeInitialBindings.get( shape, () => (0, import_shared.getArrowBindings)(this.editor, shape) ); const terminals = (0, import_shared.getArrowTerminalsInArrowSpace)(this.editor, shape, bindings); const { start, end } = (0, import_editor.structuredClone)(shape.props); let { bend } = shape.props; if (!bindings.start) { start.x = terminals.start.x * scaleX; start.y = terminals.start.y * scaleY; } if (!bindings.end) { end.x = terminals.end.x * scaleX; end.y = terminals.end.y * scaleY; } const mx = Math.abs(scaleX); const my = Math.abs(scaleY); const startNormalizedAnchor = bindings?.start ? import_editor.Vec.From(bindings.start.props.normalizedAnchor) : null; const endNormalizedAnchor = bindings?.end ? import_editor.Vec.From(bindings.end.props.normalizedAnchor) : null; if (scaleX < 0 && scaleY >= 0) { if (bend !== 0) { bend *= -1; bend *= Math.max(mx, my); } if (startNormalizedAnchor) { startNormalizedAnchor.x = 1 - startNormalizedAnchor.x; } if (endNormalizedAnchor) { endNormalizedAnchor.x = 1 - endNormalizedAnchor.x; } } else if (scaleX >= 0 && scaleY < 0) { if (bend !== 0) { bend *= -1; bend *= Math.max(mx, my); } if (startNormalizedAnchor) { startNormalizedAnchor.y = 1 - startNormalizedAnchor.y; } if (endNormalizedAnchor) { endNormalizedAnchor.y = 1 - endNormalizedAnchor.y; } } else if (scaleX >= 0 && scaleY >= 0) { if (bend !== 0) { bend *= Math.max(mx, my); } } else if (scaleX < 0 && scaleY < 0) { if (bend !== 0) { bend *= Math.max(mx, my); } if (startNormalizedAnchor) { startNormalizedAnchor.x = 1 - startNormalizedAnchor.x; startNormalizedAnchor.y = 1 - startNormalizedAnchor.y; } if (endNormalizedAnchor) { endNormalizedAnchor.x = 1 - endNormalizedAnchor.x; endNormalizedAnchor.y = 1 - endNormalizedAnchor.y; } } if (bindings.start && startNormalizedAnchor) { (0, import_shared.createOrUpdateArrowBinding)(this.editor, shape, bindings.start.toId, { ...bindings.start.props, normalizedAnchor: startNormalizedAnchor.toJson() }); } if (bindings.end && endNormalizedAnchor) { (0, import_shared.createOrUpdateArrowBinding)(this.editor, shape, bindings.end.toId, { ...bindings.end.props, normalizedAnchor: endNormalizedAnchor.toJson() }); } const next = { props: { start, end, bend } }; return next; } onDoubleClickHandle(shape, handle) { switch (handle.id) { case ArrowHandles.Start: { return { id: shape.id, type: shape.type, props: { ...shape.props, arrowheadStart: shape.props.arrowheadStart === "none" ? "arrow" : "none" } }; } case ArrowHandles.End: { return { id: shape.id, type: shape.type, props: { ...shape.props, arrowheadEnd: shape.props.arrowheadEnd === "none" ? "arrow" : "none" } }; } } } component(shape) { const { editor } = this; const theme = (0, import_useDefaultColorTheme.useDefaultColorTheme)(); const shouldDisplayHandles = (0, import_editor.useValue)( "should display handles", () => { const { editor: editor2 } = this; return !editor2.getIsReadonly() && editor2.getOnlySelectedShapeId() === shape.id && editor2.isInAny( "select.idle", "select.pointing_handle", "select.dragging_handle", "select.translating", "arrow.dragging" ); }, [editor, shape.id] ); const isSelected = (0, import_editor.useValue)( "is selected", () => editor.getOnlySelectedShape()?.id === shape.id, [editor, shape.id] ); const isEditing = (0, import_editor.useValue)("is editing", () => editor.getEditingShapeId() === shape.id, [ editor, shape.id ]); const info = (0, import_getArrowInfo.getArrowInfo)(editor, shape); if (!info?.isValid) return null; const labelPosition = (0, import_arrowLabel.getArrowLabelPosition)(editor, shape, isEditing); const showArrowLabel = isEditing || !(0, import_richText.isEmptyRichText)(shape.props.richText); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_editor.SVGContainer, { style: { minWidth: 50, minHeight: 50 }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowSvg, { shape, shouldDisplayHandles }), shape.props.kind === "elbow" && import_editor.debugFlags.debugElbowArrows.get() && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ElbowArrowDebug.ElbowArrowDebug, { arrow: shape }) ] }), showArrowLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_RichTextLabel.RichTextLabel, { shapeId: shape.id, type: "arrow", font: shape.props.font, fontSize: (0, import_arrowLabel.getArrowLabelFontSize)(shape), lineHeight: import_default_shape_constants.TEXT_PROPS.lineHeight, align: "middle", verticalAlign: "middle", labelColor: (0, import_editor.getColorValue)(theme, shape.props.labelColor, "solid"), richText: shape.props.richText, textWidth: labelPosition.box.w - import_default_shape_constants.ARROW_LABEL_PADDING * 2 * shape.props.scale, isSelected, padding: 0, showTextOutline: this.options.showTextOutline, style: { transform: `translate(${labelPosition.box.center.x}px, ${labelPosition.box.center.y}px)` } } ) ] }); } indicator(shape) { const isEditing = (0, import_editor.useIsEditing)(shape.id); const clipPathId = (0, import_editor.useSharedSafeId)(shape.id + "_clip"); const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); if (!info) return null; const { start, end } = (0, import_shared.getArrowTerminalsInArrowSpace)(this.editor, shape, info?.bindings); const geometry = this.editor.getShapeGeometry(shape); const bounds = geometry.bounds; const isEmpty = (0, import_richText.isEmptyRichText)(shape.props.richText); const labelGeometry = isEditing || !isEmpty ? geometry.children[1] : null; if (import_editor.Vec.Equals(start, end)) return null; const strokeWidth = import_default_shape_constants.STROKE_SIZES[shape.props.size] * shape.props.scale; const as = info.start.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "start", strokeWidth); const ae = info.end.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "end", strokeWidth); const includeClipPath = as && info.start.arrowhead !== "arrow" || ae && info.end.arrowhead !== "arrow" || !!labelGeometry; const labelBounds = labelGeometry ? labelGeometry.getBounds() : new import_editor.Box(0, 0, 0, 0); if (isEditing && labelGeometry) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "rect", { x: (0, import_editor.toDomPrecision)(labelBounds.x), y: (0, import_editor.toDomPrecision)(labelBounds.y), width: labelBounds.w, height: labelBounds.h, rx: 3.5 * shape.props.scale, ry: 3.5 * shape.props.scale } ); } const clipStartArrowhead = !(info.start.arrowhead === "none" || info.start.arrowhead === "arrow"); const clipEndArrowhead = !(info.end.arrowhead === "none" || info.end.arrowhead === "arrow"); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [ includeClipPath && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ArrowClipPath, { radius: 3.5 * shape.props.scale, hasText: !isEmpty, bounds, labelBounds, as: clipStartArrowhead && as ? as : "", ae: clipEndArrowhead && ae ? ae : "" } ) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "g", { style: { clipPath: includeClipPath ? `url(#${clipPathId})` : void 0, WebkitClipPath: includeClipPath ? `url(#${clipPathId})` : void 0 }, children: [ includeClipPath && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "rect", { x: bounds.minX - 100, y: bounds.minY - 100, width: bounds.width + 200, height: bounds.height + 200, opacity: 0 } ), (0, import_ArrowPath.getArrowBodyPath)( shape, info, shape.props.dash === "draw" ? { style: "draw", randomSeed: shape.id, strokeWidth: 1, passes: 1, offset: 0, roundness: strokeWidth * 2, props: { strokeWidth: void 0 } } : { style: "solid", strokeWidth: 1, props: { strokeWidth: void 0 } } ) ] } ), as && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: as }), ae && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: ae }), labelGeometry && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "rect", { x: (0, import_editor.toDomPrecision)(labelBounds.x), y: (0, import_editor.toDomPrecision)(labelBounds.y), width: labelBounds.w, height: labelBounds.h, rx: 3.5, ry: 3.5 } ) ] }); } useLegacyIndicator() { return false; } getIndicatorPath(shape) { const info = (0, import_getArrowInfo.getArrowInfo)(this.editor, shape); if (!info) return void 0; const isEditing = this.editor.getEditingShapeId() === shape.id; const { start, end } = (0, import_shared.getArrowTerminalsInArrowSpace)(this.editor, shape, info?.bindings); const geometry = this.editor.getShapeGeometry(shape); const isEmpty = (0, import_richText.isEmptyRichText)(shape.props.richText); const labelGeometry = isEditing || !isEmpty ? geometry.children[1] : null; if (import_editor.Vec.Equals(start, end)) return void 0; const strokeWidth = import_default_shape_constants.STROKE_SIZES[shape.props.size] * shape.props.scale; if (isEditing && labelGeometry) { const labelBounds = labelGeometry.getBounds(); const path = new Path2D(); path.roundRect( labelBounds.x, labelBounds.y, labelBounds.w, labelBounds.h, 3.5 * shape.props.scale ); return path; } const isForceSolid = this.editor.getEfficientZoomLevel() < 0.25 / shape.props.scale; const bodyPathBuilder = (0, import_ArrowPath.getArrowBodyPathBuilder)(info); const bodyPath2D = bodyPathBuilder.toPath2D( shape.props.dash === "draw" && !isForceSolid ? { style: "draw", randomSeed: shape.id, strokeWidth: 1, passes: 1, offset: 0, roundness: strokeWidth * 2 } : { style: "solid", strokeWidth: 1 } ); const as = info.start.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "start", strokeWidth); const ae = info.end.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "end", strokeWidth); const clipStartArrowhead = !!(as && info.start.arrowhead !== "arrow"); const clipEndArrowhead = !!(ae && info.end.arrowhead !== "arrow"); const needsClipping = labelGeometry || clipStartArrowhead || clipEndArrowhead; if (needsClipping) { const bounds = geometry.bounds; const clipPath = new Path2D(); clipPath.rect(bounds.minX - 100, bounds.minY - 100, bounds.width + 200, bounds.height + 200); if (labelGeometry) { const labelBounds = labelGeometry.getBounds(); const radius = 3.5 * shape.props.scale; const lb = labelBounds; clipPath.moveTo(lb.x, lb.y + radius); clipPath.lineTo(lb.x, lb.maxY - radius); clipPath.arcTo(lb.x, lb.maxY, lb.x + radius, lb.maxY, radius); clipPath.lineTo(lb.maxX - radius, lb.maxY); clipPath.arcTo(lb.maxX, lb.maxY, lb.maxX, lb.maxY - radius, radius); clipPath.lineTo(lb.maxX, lb.y + radius); clipPath.arcTo(lb.maxX, lb.y, lb.maxX - radius, lb.y, radius); clipPath.lineTo(lb.x + radius, lb.y); clipPath.arcTo(lb.x, lb.y, lb.x, lb.y + radius, radius); clipPath.closePath(); } if (clipStartArrowhead && as) { clipPath.addPath(new Path2D(as)); } if (clipEndArrowhead && ae) { clipPath.addPath(new Path2D(ae)); } const additionalPaths = []; if (as) additionalPaths.push(new Path2D(as)); if (ae) additionalPaths.push(new Path2D(ae)); if (labelGeometry) { const labelBounds = labelGeometry.getBounds(); const labelPath = new Path2D(); labelPath.roundRect(labelBounds.x, labelBounds.y, labelBounds.w, labelBounds.h, 3.5); additionalPaths.push(labelPath); } return { path: bodyPath2D, clipPath, additionalPaths }; } const combinedPath = new Path2D(); combinedPath.addPath(bodyPath2D); if (as) { combinedPath.addPath(new Path2D(as)); } if (ae) { combinedPath.addPath(new Path2D(ae)); } return combinedPath; } onEditStart(shape) { if ((0, import_richText.isEmptyRichText)(shape.props.richText)) { const labelPosition = (0, import_arrowLabel.getArrowLabelDefaultPosition)(this.editor, shape); this.editor.updateShape({ id: shape.id, type: shape.type, props: { labelPosition } }); } } toSvg(shape, ctx) { ctx.addExportDef((0, import_defaultStyleDefs.getFillDefForExport)(shape.props.fill)); const theme = (0, import_editor.getDefaultColorTheme)(ctx); const scaleFactor = 1 / shape.props.scale; const showArrowLabel = !(0, import_richText.isEmptyRichText)(shape.props.richText); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { transform: `scale(${scaleFactor})`, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowSvg, { shape, shouldDisplayHandles: false }), showArrowLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_RichTextLabel.RichTextSVG, { fontSize: (0, import_arrowLabel.getArrowLabelFontSize)(shape), font: shape.props.font, align: "middle", verticalAlign: "middle", labelColor: (0, import_editor.getColorValue)(theme, shape.props.labelColor, "solid"), richText: shape.props.richText, bounds: (0, import_arrowLabel.getArrowLabelPosition)(this.editor, shape, false).box.clone().expandBy(-import_default_shape_constants.ARROW_LABEL_PADDING * shape.props.scale), padding: 0, showTextOutline: this.options.showTextOutline } ) ] }); } getCanvasSvgDefs() { return [ (0, import_defaultStyleDefs.getFillDefForCanvas)(), { key: `arrow:dot`, component: ArrowheadDotDef }, { key: `arrow:cross`, component: ArrowheadCrossDef } ]; } getInterpolatedProps(startShape, endShape, progress) { return { ...progress > 0.5 ? endShape.props : startShape.props, scale: (0, import_editor.lerp)(startShape.props.scale, endShape.props.scale, progress), start: { x: (0, import_editor.lerp)(startShape.props.start.x, endShape.props.start.x, progress), y: (0, import_editor.lerp)(startShape.props.start.y, endShape.props.start.y, progress) }, end: { x: (0, import_editor.lerp)(startShape.props.end.x, endShape.props.end.x, progress), y: (0, import_editor.lerp)(startShape.props.end.y, endShape.props.end.y, progress) }, bend: (0, import_editor.lerp)(startShape.props.bend, endShape.props.bend, progress), labelPosition: (0, import_editor.lerp)(startShape.props.labelPosition, endShape.props.labelPosition, progress) }; } } function getArrowLength(editor, shape) { const info = (0, import_getArrowInfo.getArrowInfo)(editor, shape); return info.type === "straight" ? import_editor.Vec.Dist(info.start.handle, info.end.handle) : info.type === "arc" ? Math.abs(info.handleArc.length) : info.route.distance; } const ArrowSvg = (0, import_editor.track)(function ArrowSvg2({ shape, shouldDisplayHandles }) { const editor = (0, import_editor.useEditor)(); const theme = (0, import_useDefaultColorTheme.useDefaultColorTheme)(); const info = (0, import_getArrowInfo.getArrowInfo)(editor, shape); const isForceSolid = (0, import_useEfficientZoomThreshold.useEfficientZoomThreshold)(0.25 / shape.props.scale); const clipPathId = (0, import_editor.useSharedSafeId)(shape.id + "_clip"); const arrowheadDotId = (0, import_editor.useSharedSafeId)("arrowhead-dot"); const arrowheadCrossId = (0, import_editor.useSharedSafeId)("arrowhead-cross"); const isEditing = (0, import_editor.useIsEditing)(shape.id); const geometry = editor.getShapeGeometry(shape); if (!geometry) return null; const bounds = import_editor.Box.ZeroFix(geometry.bounds); const bindings = (0, import_shared.getArrowBindings)(editor, shape); const isEmpty = (0, import_richText.isEmptyRichText)(shape.props.richText); if (!info?.isValid) return null; const strokeWidth = import_default_shape_constants.STROKE_SIZES[shape.props.size] * shape.props.scale; const as = info.start.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "start", strokeWidth); const ae = info.end.arrowhead && (0, import_arrowheads.getArrowheadPathForType)(info, "end", strokeWidth); let handlePath = null; if (shouldDisplayHandles && (bindings.start || bindings.end)) { handlePath = (0, import_ArrowPath.getArrowHandlePath)(info, { style: "dashed", start: "skip", end: "skip", lengthRatio: 2.5, strokeWidth: 2 / editor.getEfficientZoomLevel(), props: { className: "tl-arrow-hint", markerStart: bindings.start ? bindings.start.props.isExact ? "" : bindings.start.props.isPrecise ? `url(#${arrowheadCrossId})` : `url(#${arrowheadDotId})` : "", markerEnd: bindings.end ? bindings.end.props.isExact ? "" : bindings.end.props.isPrecise ? `url(#${arrowheadCrossId})` : `url(#${arrowheadDotId})` : "", opacity: 0.16 } }); } const labelPosition = (0, import_arrowLabel.getArrowLabelPosition)(editor, shape, isEditing); const clipStartArrowhead = !(info.start.arrowhead === "none" || info.start.arrowhead === "arrow"); const clipEndArrowhead = !(info.end.arrowhead === "none" || info.end.arrowhead === "arrow"); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("clipPath", { id: clipPathId, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ArrowClipPath, { radius: 3.5 * shape.props.scale, hasText: isEditing || !isEmpty, bounds, labelBounds: labelPosition.box, as: clipStartArrowhead && as ? as : "", ae: clipEndArrowhead && ae ? ae : "" } ) }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "g", { fill: "none", stroke: (0, import_editor.getColorValue)(theme, shape.props.color, "solid"), strokeWidth, strokeLinejoin: "round", strokeLinecap: "round", pointerEvents: "none", children: [ handlePath, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "g", { style: { clipPath: `url(#${clipPathId})`, WebkitClipPath: `url(#${clipPathId})` }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "rect", { x: (0, import_editor.toDomPrecision)(bounds.minX - 100), y: (0, import_editor.toDomPrecision)(bounds.minY - 100), width: (0, import_editor.toDomPrecision)(bounds.width + 200), height: (0, import_editor.toDomPrecision)(bounds.height + 200), opacity: 0 } ), (0, import_ArrowPath.getArrowBodyPath)(shape, info, { style: shape.props.dash, strokeWidth, forceSolid: isForceSolid, randomSeed: shape.id }) ] } ), as && clipStartArrowhead && shape.props.fill !== "none" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_ShapeFill.ShapeFill, { theme, d: as, color: shape.props.color, fill: shape.props.fill, scale: shape.props.scale } ), ae && clipEndArrowhead && shape.props.fill !== "none" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_ShapeFill.ShapeFill, { theme, d: ae, color: shape.props.color, fill: shape.props.fill, scale: shape.props.scale } ), as && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: as }), ae && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: ae }) ] } ) ] }); }); function ArrowClipPath({ radius, hasText, bounds, labelBounds, as, ae }) { const path = (0, import_react.useMemo)(() => { const path2 = new import_PathBuilder.PathBuilder(); path2.moveTo(bounds.left - 100, bounds.top - 100).lineTo(bounds.right + 100, bounds.top - 100).lineTo(bounds.right + 100, bounds.bottom + 100).lineTo(bounds.left - 100, bounds.bottom + 100).close(); if (hasText) { path2.moveTo(labelBounds.left, labelBounds.top + radius).lineTo(labelBounds.left, labelBounds.bottom - radius).circularArcTo(radius, false, false, labelBounds.left + radius, labelBounds.bottom).lineTo(labelBounds.right - radius, labelBounds.bottom).circularArcTo(radius, false, false, labelBounds.right, labelBounds.bottom - radius).lineTo(labelBounds.right, labelBounds.top + radius).circularArcTo(radius, false, false, labelBounds.right - radius, labelBounds.top).lineTo(labelBounds.left + radius, labelBounds.top).circularArcTo(radius, false, false, labelBounds.left, labelBounds.top + radius).close(); } return path2.toD(); }, [ radius, hasText, bounds.bottom, bounds.left, bounds.right, bounds.top, labelBounds.bottom, labelBounds.left, labelBounds.right, labelBounds.top ]); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: `${path}${as}${ae}` }); } const shapeAtTranslationStart = /* @__PURE__ */ new WeakMap(); function ArrowheadDotDef() { const id = (0, import_editor.useSharedSafeId)("arrowhead-dot"); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("marker", { id, className: "tl-arrow-hint", refX: "3.0", refY: "3.0", orient: "0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "3", cy: "3", r: "2", strokeDasharray: "100%" }) }); } function ArrowheadCrossDef() { const id = (0, import_editor.useSharedSafeId)("arrowhead-cross"); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("marker", { id, className: "tl-arrow-hint", refX: "3.0", refY: "3.0", orient: "auto", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "1.5", y1: "1.5", x2: "4.5", y2: "4.5", strokeDasharray: "100%" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "1.5", y1: "4.5", x2: "4.5", y2: "1.5", strokeDasharray: "100%" }) ] }); } function anglesAreApproximatelyParallel(a, b, tolerance = 1e-4) { const diff = Math.abs(a - b); const isParallel = diff < tolerance; const isFlippedParallel = Math.abs(diff - Math.PI) < tolerance; const is360Parallel = Math.abs(diff - import_editor.PI2) < tolerance; return { isParallel: isParallel || is360Parallel, isFlippedParallel }; } //# sourceMappingURL=ArrowShapeUtil.js.map