UNPKG

tldraw

Version:

A tiny little drawing editor.

365 lines (364 loc) • 10.8 kB
import { jsx } from "react/jsx-runtime"; import { CubicSpline2d, Group2d, Polyline2d, SVGContainer, ShapeUtil, Vec, WeakCache, ZERO_INDEX_KEY, getIndexAbove, getIndexBetween, getIndices, getPerfectDashProps, lerp, lineShapeMigrations, lineShapeProps, mapObjectMapValues, maybeSnapToGrid, sortByIndex } from "@tldraw/editor"; import { STROKE_SIZES } from "../arrow/shared.mjs"; import { useDefaultColorTheme } from "../shared/useDefaultColorTheme.mjs"; import { getLineDrawPath, getLineIndicatorPath } from "./components/getLinePath.mjs"; import { getDrawLinePathData } from "./line-helpers.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; } 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) { return getGeometryForLineShape(shape); } getHandles(shape) { return handlesCache.get(shape.props, () => { const spline = getGeometryForLineShape(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.segments[i]; const point = segment.midPoint(); 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 }) { if (handle.type !== "vertex") return; 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 } } } }; } component(shape) { return /* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx(LineShapeSvg, { shape }) }); } indicator(shape) { const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale; const spline = getGeometryForLineShape(shape); const { dash } = shape.props; let path; if (shape.props.spline === "line") { const outline = spline.points; if (dash === "solid" || dash === "dotted" || dash === "dashed") { path = "M" + outline[0] + "L" + outline.slice(1); } else { const [innerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth); path = innerPathData; } } else { path = getLineIndicatorPath(shape, spline, strokeWidth); } return /* @__PURE__ */ jsx("path", { d: path }); } 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 = getGeometryForLineShape(shape).segments.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); } function getGeometryForLineShape(shape) { const points = linePointsToArray(shape).map(Vec.From); switch (shape.props.spline) { case "cubic": { return new CubicSpline2d({ points }); } case "line": { return new Polyline2d({ points }); } } } function LineShapeSvg({ shape, shouldScale = false, forceSolid = false }) { const theme = useDefaultColorTheme(); const spline = getGeometryForLineShape(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; if (shape.props.spline === "line") { if (dash === "solid") { const outline = spline.points; const pathData = "M" + outline[0] + "L" + outline.slice(1); return /* @__PURE__ */ jsx( "path", { d: pathData, stroke: theme[color].solid, strokeWidth, fill: "none", transform: `scale(${scale})` } ); } if (dash === "dashed" || dash === "dotted") { return /* @__PURE__ */ jsx("g", { stroke: theme[color].solid, strokeWidth, transform: `scale(${scale})`, children: spline.segments.map((segment, i) => { const { strokeDasharray, strokeDashoffset } = forceSolid ? { strokeDasharray: "none", strokeDashoffset: "none" } : getPerfectDashProps(segment.length, strokeWidth, { style: dash, start: i > 0 ? "outset" : "none", end: i < spline.segments.length - 1 ? "outset" : "none" }); return /* @__PURE__ */ jsx( "path", { strokeDasharray, strokeDashoffset, d: segment.getSvgPathData(true), fill: "none" }, i ); }) }); } if (dash === "draw") { const outline = spline.points; const [_, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth); return /* @__PURE__ */ jsx( "path", { d: outerPathData, stroke: theme[color].solid, strokeWidth, fill: "none", transform: `scale(${scale})` } ); } } if (shape.props.spline === "cubic") { const splinePath = spline.getSvgPathData(); if (dash === "solid") { return /* @__PURE__ */ jsx( "path", { strokeWidth, stroke: theme[color].solid, fill: "none", d: splinePath, transform: `scale(${scale})` } ); } if (dash === "dashed" || dash === "dotted") { return /* @__PURE__ */ jsx("g", { stroke: theme[color].solid, strokeWidth, transform: `scale(${scale})`, children: spline.segments.map((segment, i) => { const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( segment.length, strokeWidth, { style: dash, start: i > 0 ? "outset" : "none", end: i < spline.segments.length - 1 ? "outset" : "none", forceSolid } ); return /* @__PURE__ */ jsx( "path", { strokeDasharray, strokeDashoffset, d: segment.getSvgPathData(), fill: "none" }, i ); }) }); } if (dash === "draw") { return /* @__PURE__ */ jsx( "path", { d: getLineDrawPath(shape, spline, strokeWidth), strokeWidth: 1, stroke: theme[color].solid, fill: theme[color].solid, transform: `scale(${scale})` } ); } } } export { LineShapeUtil, getGeometryForLineShape }; //# sourceMappingURL=LineShapeUtil.mjs.map