UNPKG

tldraw

Version:

A tiny little drawing editor.

305 lines (304 loc) • 8.67 kB
import { jsx } from "react/jsx-runtime"; import { Group2d, SVGContainer, ShapeUtil, Vec, WeakCache, ZERO_INDEX_KEY, assert, getColorValue, getIndexAbove, getIndexBetween, getIndices, lerp, lineShapeMigrations, lineShapeProps, mapObjectMapValues, maybeSnapToGrid, sortByIndex } from "@tldraw/editor"; import { STROKE_SIZES } from "../arrow/shared.mjs"; import { PathBuilder, PathBuilderGeometry2d } from "../shared/PathBuilder.mjs"; import { useDefaultColorTheme } from "../shared/useDefaultColorTheme.mjs"; const handlesCache = new WeakCache(); class LineShapeUtil extends ShapeUtil { static type = "line"; static props = lineShapeProps; static migrations = lineShapeMigrations; hideResizeHandles() { return true; } hideRotateHandle() { return true; } hideSelectionBoundsFg() { return true; } hideSelectionBoundsBg() { return true; } hideInMinimap() { return true; } getDefaultProps() { const [start, end] = getIndices(2); return { dash: "draw", size: "m", color: "black", spline: "line", points: { [start]: { id: start, index: start, x: 0, y: 0 }, [end]: { id: end, index: end, x: 0.1, y: 0.1 } }, scale: 1 }; } getGeometry(shape) { const geometry = getPathForLineShape(shape).toGeometry(); assert(geometry instanceof PathBuilderGeometry2d); return geometry; } getHandles(shape) { return handlesCache.get(shape.props, () => { const spline = this.getGeometry(shape); const points = linePointsToArray(shape); const results = points.map((point) => ({ ...point, id: point.index, type: "vertex", canSnap: true })); for (let i = 0; i < points.length - 1; i++) { const index = getIndexBetween(points[i].index, points[i + 1].index); const segment = spline.getSegments()[i]; const point = segment.interpolateAlongEdge(0.5); results.push({ id: index, type: "create", index, x: point.x, y: point.y, canSnap: true }); } return results.sort(sortByIndex); }); } // Events onResize(shape, info) { const { scaleX, scaleY } = info; return { props: { points: mapObjectMapValues(shape.props.points, (_, { id, index, x, y }) => ({ id, index, x: x * scaleX, y: y * scaleY })) } }; } onBeforeCreate(next) { const { props: { points } } = next; const pointKeys = Object.keys(points); if (pointKeys.length < 2) { return; } const firstPoint = points[pointKeys[0]]; const allSame = pointKeys.every((key) => { const point = points[key]; return point.x === firstPoint.x && point.y === firstPoint.y; }); if (allSame) { const lastKey = pointKeys[pointKeys.length - 1]; points[lastKey] = { ...points[lastKey], x: points[lastKey].x + 0.1, y: points[lastKey].y + 0.1 }; return next; } return; } onHandleDrag(shape, { handle }) { const newPoint = maybeSnapToGrid(new Vec(handle.x, handle.y), this.editor); return { ...shape, props: { ...shape.props, points: { ...shape.props.points, [handle.id]: { id: handle.id, index: handle.index, x: newPoint.x, y: newPoint.y } } } }; } onHandleDragStart(shape, { handle }) { if (handle.type === "create") { return { ...shape, props: { ...shape.props, points: { ...shape.props.points, [handle.index]: { id: handle.index, index: handle.index, x: handle.x, y: handle.y } } } }; } return; } component(shape) { return /* @__PURE__ */ jsx(SVGContainer, { style: { minWidth: 50, minHeight: 50 }, children: /* @__PURE__ */ jsx(LineShapeSvg, { shape }) }); } indicator(shape) { const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale; const path = getPathForLineShape(shape); const { dash } = shape.props; return path.toSvg({ style: dash === "draw" ? "draw" : "solid", strokeWidth: 1, passes: 1, randomSeed: shape.id, offset: 0, roundness: strokeWidth * 2, props: { strokeWidth: void 0 } }); } useLegacyIndicator() { return false; } getIndicatorPath(shape) { const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale; const path = getPathForLineShape(shape); const { dash } = shape.props; return path.toPath2D({ style: dash === "draw" ? "draw" : "solid", strokeWidth: 1, passes: 1, randomSeed: shape.id, offset: 0, roundness: strokeWidth * 2 }); } toSvg(shape) { return /* @__PURE__ */ jsx(LineShapeSvg, { shouldScale: true, shape }); } getHandleSnapGeometry(shape) { const points = linePointsToArray(shape); return { points, getSelfSnapPoints: (handle) => { const index = this.getHandles(shape).filter((h) => h.type === "vertex").findIndex((h) => h.id === handle.id); return points.filter((_, i) => Math.abs(i - index) > 1).map(Vec.From); }, getSelfSnapOutline: (handle) => { const index = this.getHandles(shape).filter((h) => h.type === "vertex").findIndex((h) => h.id === handle.id); const segments = this.getGeometry(shape).getSegments().filter((_, i) => i !== index - 1 && i !== index); if (!segments.length) return null; return new Group2d({ children: segments }); } }; } getInterpolatedProps(startShape, endShape, t) { const startPoints = linePointsToArray(startShape); const endPoints = linePointsToArray(endShape); const pointsToUseStart = []; const pointsToUseEnd = []; let index = ZERO_INDEX_KEY; if (startPoints.length > endPoints.length) { for (let i = 0; i < startPoints.length; i++) { pointsToUseStart[i] = { ...startPoints[i] }; if (endPoints[i] === void 0) { pointsToUseEnd[i] = { ...endPoints[endPoints.length - 1], id: index }; } else { pointsToUseEnd[i] = { ...endPoints[i], id: index }; } index = getIndexAbove(index); } } else if (endPoints.length > startPoints.length) { for (let i = 0; i < endPoints.length; i++) { pointsToUseEnd[i] = { ...endPoints[i] }; if (startPoints[i] === void 0) { pointsToUseStart[i] = { ...startPoints[startPoints.length - 1], id: index }; } else { pointsToUseStart[i] = { ...startPoints[i], id: index }; } index = getIndexAbove(index); } } else { for (let i = 0; i < endPoints.length; i++) { pointsToUseStart[i] = startPoints[i]; pointsToUseEnd[i] = endPoints[i]; } } return { ...(t > 0.5 ? endShape.props : startShape.props), points: Object.fromEntries( pointsToUseStart.map((point, i) => { const endPoint = pointsToUseEnd[i]; return [ point.id, { ...point, x: lerp(point.x, endPoint.x, t), y: lerp(point.y, endPoint.y, t) } ]; }) ), scale: lerp(startShape.props.scale, endShape.props.scale, t) }; } } function linePointsToArray(shape) { return Object.values(shape.props.points).sort(sortByIndex); } const pathCache = new WeakCache(); function getPathForLineShape(shape) { return pathCache.get(shape, () => { const points = linePointsToArray(shape).map(Vec.From); switch (shape.props.spline) { case "cubic": { return PathBuilder.cubicSplineThroughPoints(points, { endOffsets: 0 }); } case "line": { return PathBuilder.lineThroughPoints(points, { endOffsets: 0 }); } } }); } function LineShapeSvg({ shape, shouldScale = false, forceSolid = false }) { const theme = useDefaultColorTheme(); const path = getPathForLineShape(shape); const { dash, color, size } = shape.props; const scaleFactor = 1 / shape.props.scale; const scale = shouldScale ? scaleFactor : 1; const strokeWidth = STROKE_SIZES[size] * shape.props.scale; return path.toSvg({ style: dash, strokeWidth, forceSolid, randomSeed: shape.id, props: { transform: `scale(${scale})`, stroke: getColorValue(theme, color, "solid"), fill: "none" } }); } export { LineShapeUtil }; //# sourceMappingURL=LineShapeUtil.mjs.map