UNPKG

tldraw

Version:

A tiny little drawing editor.

260 lines (259 loc) • 9.85 kB
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