tldraw
Version:
A tiny little drawing editor.
305 lines (304 loc) • 8.67 kB
JavaScript
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