tldraw
Version:
A tiny little drawing editor.
1,043 lines (1,042 loc) • 43.4 kB
JavaScript
"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