tldraw
Version:
A tiny little drawing editor.
260 lines (259 loc) • 9.85 kB
JavaScript
import {
Arc2d,
Box,
Circle2d,
Edge2d,
Polygon2d,
Vec,
WeakCache,
angleDistance,
clamp,
getPointOnCircle,
intersectCirclePolygon,
intersectLineSegmentPolygon
} from "@tldraw/editor";
import {
ARROW_LABEL_FONT_SIZES,
ARROW_LABEL_PADDING,
FONT_FAMILIES,
LABEL_TO_ARROW_PADDING,
STROKE_SIZES,
TEXT_PROPS
} from "../shared/default-shape-constants.mjs";
import { getArrowLength } from "./ArrowShapeUtil.mjs";
import { getArrowInfo } from "./shared.mjs";
const labelSizeCacheCache = new WeakCache();
function getLabelSizeCache(editor) {
return labelSizeCacheCache.get(editor, () => {
return editor.store.createComputedCache("arrowLabelSize", (shape) => {
const info = getArrowInfo(editor, shape);
let width = 0;
let height = 0;
const bodyGeom = info.isStraight ? new Edge2d({
start: Vec.From(info.start.point),
end: Vec.From(info.end.point)
}) : new Arc2d({
center: Vec.Cast(info.handleArc.center),
start: Vec.Cast(info.start.point),
end: Vec.Cast(info.end.point),
sweepFlag: info.bodyArc.sweepFlag,
largeArcFlag: info.bodyArc.largeArcFlag
});
if (shape.props.text.trim()) {
const bodyBounds = bodyGeom.bounds;
const fontSize = getArrowLabelFontSize(shape);
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize,
maxWidth: null
});
width = w;
height = h;
let shouldSquish = false;
if (bodyBounds.width > bodyBounds.height) {
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w));
shouldSquish = true;
} else if (width > 16 * fontSize) {
width = 16 * fontSize;
shouldSquish = true;
}
if (shouldSquish) {
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize,
maxWidth: width
}
);
width = squishedWidth;
height = squishedHeight;
}
}
return new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2 * shape.props.scale);
});
});
}
function getArrowLabelSize(editor, shape) {
if (shape.props.text.trim() === "") {
return new Vec(0, 0).addScalar(ARROW_LABEL_PADDING * 2 * shape.props.scale);
}
return getLabelSizeCache(editor).get(shape.id) ?? new Vec(0, 0);
}
function getLabelToArrowPadding(shape) {
const strokeWidth = STROKE_SIZES[shape.props.size];
const labelToArrowPadding = (LABEL_TO_ARROW_PADDING + (strokeWidth - STROKE_SIZES.s) * 2 + (strokeWidth === STROKE_SIZES.xl ? 20 : 0)) * shape.props.scale;
return labelToArrowPadding;
}
function getStraightArrowLabelRange(editor, shape, info) {
const labelSize = getArrowLabelSize(editor, shape);
const labelToArrowPadding = getLabelToArrowPadding(shape);
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding);
const endOffset = Vec.Nudge(info.end.point, info.start.point, labelToArrowPadding);
const intersectionPoints = intersectLineSegmentPolygon(
startOffset,
endOffset,
Box.FromCenter(info.middle, labelSize).corners
);
if (!intersectionPoints || intersectionPoints.length !== 2) {
return { start: 0.5, end: 0.5 };
}
let [startIntersect, endIntersect] = intersectionPoints;
if (Vec.Dist2(startIntersect, startOffset) > Vec.Dist2(endIntersect, startOffset)) {
;
[endIntersect, startIntersect] = intersectionPoints;
}
const startConstrained = startOffset.add(Vec.Sub(info.middle, startIntersect));
const endConstrained = endOffset.add(Vec.Sub(info.middle, endIntersect));
const start = Vec.Dist(info.start.point, startConstrained) / info.length;
const end = Vec.Dist(info.start.point, endConstrained) / info.length;
return { start, end };
}
function getCurvedArrowLabelRange(editor, shape, info) {
const labelSize = getArrowLabelSize(editor, shape);
const labelToArrowPadding = getLabelToArrowPadding(shape);
const direction = Math.sign(shape.props.bend);
const labelToArrowPaddingRad = labelToArrowPadding / info.handleArc.radius * direction;
const startOffsetAngle = Vec.Angle(info.bodyArc.center, info.start.point) - labelToArrowPaddingRad;
const endOffsetAngle = Vec.Angle(info.bodyArc.center, info.end.point) + labelToArrowPaddingRad;
const startOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, startOffsetAngle);
const endOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, endOffsetAngle);
const dbg = [];
const startIntersections = intersectArcPolygon(
info.bodyArc.center,
info.bodyArc.radius,
startOffsetAngle,
endOffsetAngle,
direction,
Box.FromCenter(startOffset, labelSize).corners
);
dbg.push(
new Polygon2d({
points: Box.FromCenter(startOffset, labelSize).corners,
debugColor: "lime",
isFilled: false,
ignore: true
})
);
const endIntersections = intersectArcPolygon(
info.bodyArc.center,
info.bodyArc.radius,
startOffsetAngle,
endOffsetAngle,
direction,
Box.FromCenter(endOffset, labelSize).corners
);
dbg.push(
new Polygon2d({
points: Box.FromCenter(endOffset, labelSize).corners,
debugColor: "lime",
isFilled: false,
ignore: true
})
);
for (const pt of [
...(startIntersections ?? []),
...(endIntersections ?? []),
startOffset,
endOffset
]) {
dbg.push(
new Circle2d({
x: pt.x - 3,
y: pt.y - 3,
radius: 3,
isFilled: false,
debugColor: "magenta",
ignore: true
})
);
}
const startConstrained = (startIntersections && furthest(info.start.point, startIntersections)) ?? info.middle;
const endConstrained = (endIntersections && furthest(info.end.point, endIntersections)) ?? info.middle;
const startAngle = Vec.Angle(info.bodyArc.center, info.start.point);
const endAngle = Vec.Angle(info.bodyArc.center, info.end.point);
const constrainedStartAngle = Vec.Angle(info.bodyArc.center, startConstrained);
const constrainedEndAngle = Vec.Angle(info.bodyArc.center, endConstrained);
if (angleDistance(startAngle, constrainedStartAngle, direction) > angleDistance(startAngle, constrainedEndAngle, direction)) {
return { start: 0.5, end: 0.5, dbg };
}
const fullDistance = angleDistance(startAngle, endAngle, direction);
const start = angleDistance(startAngle, constrainedStartAngle, direction) / fullDistance;
const end = angleDistance(startAngle, constrainedEndAngle, direction) / fullDistance;
return { start, end, dbg };
}
function getArrowLabelPosition(editor, shape) {
let labelCenter;
const debugGeom = [];
const info = getArrowInfo(editor, shape);
const arrowheadInfo = {
hasStartBinding: !!info.bindings.start,
hasEndBinding: !!info.bindings.end,
hasStartArrowhead: info.start.arrowhead !== "none",
hasEndArrowhead: info.end.arrowhead !== "none"
};
if (info.isStraight) {
const range = getStraightArrowLabelRange(editor, shape, info);
const clampedPosition = getClampedPosition(editor, shape, range, arrowheadInfo);
labelCenter = Vec.Lrp(info.start.point, info.end.point, clampedPosition);
} else {
const range = getCurvedArrowLabelRange(editor, shape, info);
if (range.dbg) debugGeom.push(...range.dbg);
const clampedPosition = getClampedPosition(editor, shape, range, arrowheadInfo);
const labelAngle = interpolateArcAngles(
Vec.Angle(info.bodyArc.center, info.start.point),
Vec.Angle(info.bodyArc.center, info.end.point),
Math.sign(shape.props.bend),
clampedPosition
);
labelCenter = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, labelAngle);
}
const labelSize = getArrowLabelSize(editor, shape);
return { box: Box.FromCenter(labelCenter, labelSize), debugGeom };
}
function getClampedPosition(editor, shape, range, arrowheadInfo) {
const { hasEndArrowhead, hasEndBinding, hasStartBinding, hasStartArrowhead } = arrowheadInfo;
const arrowLength = getArrowLength(editor, shape);
let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead || hasStartBinding ? range.start : 0,
hasEndArrowhead || hasEndBinding ? range.end : 1
);
const snapDistance = Math.min(0.02, 500 / arrowLength * 0.02);
clampedPosition = clampedPosition >= 0.5 - snapDistance && clampedPosition <= 0.5 + snapDistance ? 0.5 : clampedPosition;
return clampedPosition;
}
function intersectArcPolygon(center, radius, angleStart, angleEnd, direction, polygon) {
const intersections = intersectCirclePolygon(center, radius, polygon);
const fullArcDistance = angleDistance(angleStart, angleEnd, direction);
return intersections?.filter((pt) => {
const pDistance = angleDistance(angleStart, Vec.Angle(center, pt), direction);
return pDistance >= 0 && pDistance <= fullArcDistance;
});
}
function furthest(from, candidates) {
let furthest2 = null;
let furthestDist = -Infinity;
for (const candidate of candidates) {
const dist = Vec.Dist2(from, candidate);
if (dist > furthestDist) {
furthest2 = candidate;
furthestDist = dist;
}
}
return furthest2;
}
function interpolateArcAngles(angleStart, angleEnd, direction, t) {
const dist = angleDistance(angleStart, angleEnd, direction);
return angleStart + dist * t * direction * -1;
}
function getArrowLabelFontSize(shape) {
return ARROW_LABEL_FONT_SIZES[shape.props.size] * shape.props.scale;
}
export {
getArrowLabelFontSize,
getArrowLabelPosition
};
//# sourceMappingURL=arrowLabel.mjs.map