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