tldraw
Version:
A tiny little drawing editor.
777 lines (776 loc) • 28 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var PathBuilder_exports = {};
__export(PathBuilder_exports, {
PathBuilder: () => PathBuilder,
PathBuilderGeometry2d: () => PathBuilderGeometry2d
});
module.exports = __toCommonJS(PathBuilder_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
class PathBuilder {
static lineThroughPoints(points, opts) {
const path = new PathBuilder();
path.moveTo(points[0].x, points[0].y, { ...opts, offset: opts?.endOffsets ?? opts?.offset });
for (let i = 1; i < points.length; i++) {
const isLast = i === points.length - 1;
path.lineTo(points[i].x, points[i].y, isLast ? { offset: opts?.endOffsets } : void 0);
}
return path;
}
static cubicSplineThroughPoints(points, opts) {
const path = new PathBuilder();
const len = points.length;
const last = len - 2;
const k = 1.25;
path.moveTo(points[0].x, points[0].y, { ...opts, offset: opts?.endOffsets ?? opts?.offset });
for (let i = 0; i < len - 1; i++) {
const p0 = i === 0 ? points[0] : points[i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = i === last ? p2 : points[i + 2];
let cp1x, cp1y, cp2x, cp2y;
if (i === 0) {
cp1x = p0.x;
cp1y = p0.y;
} else {
cp1x = p1.x + (p2.x - p0.x) / 6 * k;
cp1y = p1.y + (p2.y - p0.y) / 6 * k;
}
let pointOpts = void 0;
if (i === last) {
cp2x = p2.x;
cp2y = p2.y;
pointOpts = { offset: opts?.endOffsets };
} else {
cp2x = p2.x - (p3.x - p1.x) / 6 * k;
cp2y = p2.y - (p3.y - p1.y) / 6 * k;
}
path.cubicBezierTo(p2.x, p2.y, cp1x, cp1y, cp2x, cp2y, pointOpts);
}
return path;
}
constructor() {
}
/** @internal */
commands = [];
lastMoveTo = null;
assertHasMoveTo() {
(0, import_editor.assert)(this.lastMoveTo, "Start an SVGPathBuilder with `.moveTo()`");
return this.lastMoveTo;
}
moveTo(x, y, opts) {
this.lastMoveTo = { type: "move", x, y, closeIdx: null, isClose: false, opts };
this.commands.push(this.lastMoveTo);
return this;
}
lineTo(x, y, opts) {
this.assertHasMoveTo();
this.commands.push({ type: "line", x, y, isClose: false, opts });
return this;
}
circularArcTo(radius, largeArcFlag, sweepFlag, x2, y2, opts) {
return this.arcTo(radius, radius, largeArcFlag, sweepFlag, 0, x2, y2, opts);
}
arcTo(rx, ry, largeArcFlag, sweepFlag, xAxisRotationRadians, x2, y2, opts) {
this.assertHasMoveTo();
const x1 = this.commands[this.commands.length - 1].x;
const y1 = this.commands[this.commands.length - 1].y;
if (x1 === x2 && y1 === y2) {
return this;
}
if (rx === 0 || ry === 0) {
return this.lineTo(x2, y2, opts);
}
const phi = xAxisRotationRadians;
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
let rx1 = Math.abs(rx);
let ry1 = Math.abs(ry);
const dx = (x1 - x2) / 2;
const dy = (y1 - y2) / 2;
const x1p = cosPhi * dx + sinPhi * dy;
const y1p = -sinPhi * dx + cosPhi * dy;
const lambda = x1p * x1p / (rx1 * rx1) + y1p * y1p / (ry1 * ry1);
if (lambda > 1) {
const sqrtLambda = Math.sqrt(lambda);
rx1 *= sqrtLambda;
ry1 *= sqrtLambda;
}
const sign = largeArcFlag !== sweepFlag ? 1 : -1;
const term = rx1 * rx1 * ry1 * ry1 - rx1 * rx1 * y1p * y1p - ry1 * ry1 * x1p * x1p;
const numerator = rx1 * rx1 * y1p * y1p + ry1 * ry1 * x1p * x1p;
let radicand = term / numerator;
radicand = radicand < 0 ? 0 : radicand;
const coef = sign * Math.sqrt(radicand);
const cxp = coef * (rx1 * y1p / ry1);
const cyp = coef * (-(ry1 * x1p) / rx1);
const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
const ux = (x1p - cxp) / rx1;
const uy = (y1p - cyp) / ry1;
const vx = (-x1p - cxp) / rx1;
const vy = (-y1p - cyp) / ry1;
const startAngle = Math.atan2(uy, ux);
let endAngle = Math.atan2(vy, vx);
if (!sweepFlag && endAngle > startAngle) {
endAngle -= 2 * Math.PI;
} else if (sweepFlag && endAngle < startAngle) {
endAngle += 2 * Math.PI;
}
const sweepAngle = endAngle - startAngle;
const approximateArcLength = Math.max(rx1, ry1) * Math.abs(sweepAngle);
const numSegments = Math.min(4, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 2)));
const resolutionPerSegment = Math.ceil(
(0, import_editor.getVerticesCountForArcLength)(approximateArcLength) / numSegments
);
const anglePerSegment = sweepAngle / numSegments;
const ellipsePoint = (angle) => {
return {
x: cx + rx1 * Math.cos(angle) * cosPhi - ry1 * Math.sin(angle) * sinPhi,
y: cy + rx1 * Math.cos(angle) * sinPhi + ry1 * Math.sin(angle) * cosPhi
};
};
const ellipseDerivative = (angle) => {
return {
x: -rx1 * Math.sin(angle) * cosPhi - ry1 * Math.cos(angle) * sinPhi,
y: -rx1 * Math.sin(angle) * sinPhi + ry1 * Math.cos(angle) * cosPhi
};
};
for (let i = 0; i < numSegments; i++) {
const theta1 = startAngle + i * anglePerSegment;
const theta2 = startAngle + (i + 1) * anglePerSegment;
const deltaTheta = theta2 - theta1;
const start = ellipsePoint(theta1);
const end = ellipsePoint(theta2);
const d1 = ellipseDerivative(theta1);
const d2 = ellipseDerivative(theta2);
const handleScale = 4 / 3 * Math.tan(deltaTheta / 4);
const cp1x = start.x + handleScale * d1.x;
const cp1y = start.y + handleScale * d1.y;
const cp2x = end.x - handleScale * d2.x;
const cp2y = end.y - handleScale * d2.y;
const bezierOpts = i === 0 ? opts : { ...opts, mergeWithPrevious: true };
this.cubicBezierToWithResolution(
end.x,
end.y,
cp1x,
cp1y,
cp2x,
cp2y,
bezierOpts,
resolutionPerSegment
);
}
return this;
}
cubicBezierTo(x, y, cp1X, cp1Y, cp2X, cp2Y, opts) {
return this.cubicBezierToWithResolution(x, y, cp1X, cp1Y, cp2X, cp2Y, opts);
}
cubicBezierToWithResolution(x, y, cp1X, cp1Y, cp2X, cp2Y, opts, resolution) {
this.assertHasMoveTo();
this.commands.push({
type: "cubic",
x,
y,
cp1: { x: cp1X, y: cp1Y },
cp2: { x: cp2X, y: cp2Y },
isClose: false,
opts,
resolution
});
return this;
}
close() {
const lastMoveTo = this.assertHasMoveTo();
const lastCommand = this.commands[this.commands.length - 1];
if ((0, import_editor.approximately)(lastMoveTo.x, lastCommand.x) && (0, import_editor.approximately)(lastMoveTo.y, lastCommand.y)) {
lastCommand.isClose = true;
} else {
this.commands.push({
type: "line",
x: lastMoveTo.x,
y: lastMoveTo.y,
isClose: true
});
}
lastMoveTo.closeIdx = this.commands.length - 1;
this.lastMoveTo = null;
return this;
}
toD(opts = {}) {
const { startIdx = 0, endIdx = this.commands.length, onlyFilled = false } = opts;
const parts = [];
let isSkippingCurrentLine = false;
let didAddMove = false;
let didAddNaturalMove = false;
const addMoveIfNeeded = (i) => {
if (didAddMove || i === 0) return;
didAddMove = true;
const command = this.commands[i - 1];
parts.push("M", (0, import_editor.toDomPrecision)(command.x), (0, import_editor.toDomPrecision)(command.y));
};
for (let i = startIdx; i < endIdx; i++) {
const command = this.commands[i];
switch (command.type) {
case "move": {
const isFilled = command.opts?.geometry === false ? false : command.opts?.geometry?.isFilled ?? false;
if (onlyFilled && !isFilled) {
isSkippingCurrentLine = true;
} else {
isSkippingCurrentLine = false;
didAddMove = true;
didAddNaturalMove = true;
parts.push("M", (0, import_editor.toDomPrecision)(command.x), (0, import_editor.toDomPrecision)(command.y));
}
break;
}
case "line":
if (isSkippingCurrentLine) break;
addMoveIfNeeded(i);
if (command.isClose && didAddNaturalMove) {
parts.push("Z");
} else {
parts.push("L", (0, import_editor.toDomPrecision)(command.x), (0, import_editor.toDomPrecision)(command.y));
}
break;
case "cubic":
if (isSkippingCurrentLine) break;
addMoveIfNeeded(i);
parts.push(
"C",
(0, import_editor.toDomPrecision)(command.cp1.x),
(0, import_editor.toDomPrecision)(command.cp1.y),
(0, import_editor.toDomPrecision)(command.cp2.x),
(0, import_editor.toDomPrecision)(command.cp2.y),
(0, import_editor.toDomPrecision)(command.x),
(0, import_editor.toDomPrecision)(command.y)
);
break;
default:
(0, import_editor.exhaustiveSwitchError)(command, "type");
}
}
return parts.join(" ");
}
toSvg(opts) {
if (opts.forceSolid) {
return this.toSolidSvg(opts);
}
switch (opts.style) {
case "solid":
return this.toSolidSvg(opts);
case "dashed":
case "dotted":
return this.toDashedSvg(opts);
case "draw": {
const d = this.toDrawSvg(opts);
return d;
}
default:
(0, import_editor.exhaustiveSwitchError)(opts, "style");
}
}
toPath2D(opts) {
if (opts.forceSolid || opts.style === "solid") {
return new Path2D(this.toD({ onlyFilled: opts.onlyFilled }));
}
if (opts.style === "draw") {
return new Path2D(this.toDrawD(opts));
}
return new Path2D(this.toD({ onlyFilled: opts.onlyFilled }));
}
toGeometry() {
const geometries = [];
let current = null;
for (let i = 0; i < this.commands.length; i++) {
const command = this.commands[i];
if (command.type === "move") {
if (current && current.opts?.geometry !== false) {
geometries.push(
new PathBuilderGeometry2d(this, current.startIdx, i, {
...current.opts?.geometry,
isFilled: current.opts?.geometry?.isFilled ?? false,
isClosed: current.moveCommand.closeIdx !== null
})
);
}
current = { startIdx: i, moveCommand: command, opts: command.opts, isClosed: false };
}
if (command.isClose) {
(0, import_editor.assert)(current, "No current move command");
current.isClosed = true;
}
}
if (current && current.opts?.geometry !== false) {
geometries.push(
new PathBuilderGeometry2d(this, current.startIdx, this.commands.length, {
...current.opts?.geometry,
isFilled: current.opts?.geometry?.isFilled ?? false,
isClosed: current.moveCommand.closeIdx !== null
})
);
}
(0, import_editor.assert)(geometries.length > 0);
if (geometries.length === 1) return geometries[0];
return new import_editor.Group2d({ children: geometries });
}
toSolidSvg(opts) {
const { strokeWidth, props } = opts;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeWidth, d: this.toD({ onlyFilled: opts.onlyFilled }), ...props });
}
toDashedSvg(opts) {
const {
style,
strokeWidth,
snap,
lengthRatio,
props: { markerStart, markerEnd, ...props } = {}
} = opts;
const parts = [];
let isCurrentPathClosed = false;
let isSkippingCurrentLine = false;
let currentLineOpts = void 0;
let currentRun = null;
const addCurrentRun = () => {
if (!currentRun) return;
const { startIdx, endIdx, isFirst, isLast, length, lineOpts, pathIsClosed } = currentRun;
currentRun = null;
if (startIdx === endIdx && this.commands[startIdx].type === "move") return;
const start = lineOpts?.dashStart ?? opts.start;
const end = lineOpts?.dashEnd ?? opts.end;
const { strokeDasharray, strokeDashoffset } = (0, import_editor.getPerfectDashProps)(length, strokeWidth, {
style,
snap,
lengthRatio,
start: isFirst ? start ?? (pathIsClosed ? "outset" : "none") : "outset",
end: isLast ? end ?? (pathIsClosed ? "outset" : "none") : "outset"
});
const d = this.toD({ startIdx, endIdx: endIdx + 1 });
parts.push(
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"path",
{
d,
strokeDasharray,
strokeDashoffset,
markerStart: isFirst ? markerStart : void 0,
markerEnd: isLast ? markerEnd : void 0
},
parts.length
)
);
};
for (let i = 0; i < this.commands.length; i++) {
const command = this.commands[i];
const lastCommand = this.commands[i - 1];
if (command.type === "move") {
isCurrentPathClosed = command.closeIdx !== null;
const isFilled = command.opts?.geometry === false ? false : command.opts?.geometry?.isFilled ?? false;
if (opts.onlyFilled && !isFilled) {
isSkippingCurrentLine = true;
} else {
isSkippingCurrentLine = false;
currentLineOpts = command.opts;
}
continue;
}
if (isSkippingCurrentLine) continue;
const segmentLength = this.calculateSegmentLength(lastCommand, command);
const isFirst = lastCommand.type === "move";
const isLast = command.isClose || i === this.commands.length - 1 || this.commands[i + 1]?.type === "move";
if (currentRun && command.opts?.mergeWithPrevious) {
currentRun.length += segmentLength;
currentRun.endIdx = i;
currentRun.isLast = isLast;
} else {
addCurrentRun();
currentRun = {
startIdx: i,
endIdx: i,
isFirst,
isLast,
length: segmentLength,
lineOpts: currentLineOpts,
pathIsClosed: isCurrentPathClosed
};
}
}
addCurrentRun();
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { strokeWidth, ...props, children: parts });
}
toDrawSvg(opts) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeWidth: opts.strokeWidth, d: this.toDrawD(opts), ...opts.props });
}
toDrawD(opts) {
const {
strokeWidth,
randomSeed,
offset: defaultOffset = strokeWidth / 3,
roundness: defaultRoundness = strokeWidth * 2,
passes = 2,
onlyFilled = false
} = opts;
const parts = [];
const commandInfo = this.getCommandInfo();
const drawCommands = [];
let lastMoveCommandIdx = null;
for (let i = 0; i < this.commands.length; i++) {
const command = this.commands[i];
const offset = command.opts?.offset ?? defaultOffset;
const roundness = command.opts?.roundness ?? defaultRoundness;
if (command.type === "move") {
lastMoveCommandIdx = i;
}
const nextIdx = command.isClose ? (0, import_editor.assertExists)(lastMoveCommandIdx) + 1 : !this.commands[i + 1] || this.commands[i + 1].type === "move" ? void 0 : i + 1;
const nextInfo = nextIdx !== void 0 && this.commands[nextIdx] && this.commands[nextIdx]?.type !== "move" ? commandInfo[nextIdx] : void 0;
const currentSupportsRoundness = commandsSupportingRoundness[command.type];
const nextSupportsRoundness = nextIdx !== void 0 ? commandsSupportingRoundness[this.commands[nextIdx].type] : false;
const currentInfo = commandInfo[i];
const tangentToPrev = currentInfo?.tangentEnd;
const tangentToNext = nextInfo?.tangentStart;
const roundnessClampedForAngle = currentSupportsRoundness && nextSupportsRoundness && tangentToPrev && tangentToNext && import_editor.Vec.Len2(tangentToPrev) > 0.01 && import_editor.Vec.Len2(tangentToNext) > 0.01 ? (0, import_editor.modulate)(
Math.abs(import_editor.Vec.AngleBetween(tangentToPrev, tangentToNext)),
[Math.PI / 2, Math.PI],
[roundness, 0],
true
) : 0;
const shortestDistance = Math.min(
currentInfo?.length ?? Infinity,
nextInfo?.length ?? Infinity
);
const offsetLimit = shortestDistance - roundnessClampedForAngle * 2;
const offsetAmount = (0, import_editor.clamp)(offset, 0, offsetLimit / 4);
const roundnessBeforeClampedForLength = Math.min(
roundnessClampedForAngle,
(currentInfo?.length ?? Infinity) / 4
);
const roundnessAfterClampedForLength = Math.min(
roundnessClampedForAngle,
(nextInfo?.length ?? Infinity) / 4
);
const drawCommand = {
command,
offsetAmount,
roundnessBefore: roundnessBeforeClampedForLength,
roundnessAfter: roundnessAfterClampedForLength,
tangentToPrev: commandInfo[i]?.tangentEnd,
tangentToNext: nextInfo?.tangentStart,
moveDidClose: false
};
drawCommands.push(drawCommand);
if (command.isClose && lastMoveCommandIdx !== null) {
const lastMoveCommand = drawCommands[lastMoveCommandIdx];
lastMoveCommand.moveDidClose = true;
lastMoveCommand.roundnessAfter = roundnessAfterClampedForLength;
} else if (command.type === "move") {
lastMoveCommandIdx = i;
}
}
for (let pass = 0; pass < passes; pass++) {
const random = (0, import_editor.rng)(randomSeed + pass);
let lastMoveToOffset = { x: 0, y: 0 };
let isSkippingCurrentLine = false;
for (const {
command,
offsetAmount,
roundnessBefore,
roundnessAfter,
tangentToNext,
tangentToPrev
} of drawCommands) {
const offset = command.isClose ? lastMoveToOffset : { x: random() * offsetAmount, y: random() * offsetAmount };
if (command.type === "move") {
lastMoveToOffset = offset;
const isFilled = command.opts?.geometry === false ? false : command.opts?.geometry?.isFilled ?? false;
if (onlyFilled && !isFilled) {
isSkippingCurrentLine = true;
} else {
isSkippingCurrentLine = false;
}
}
if (isSkippingCurrentLine) continue;
const offsetPoint = import_editor.Vec.Add(command, offset);
const endPoint = tangentToNext && roundnessAfter > 0 ? import_editor.Vec.Mul(tangentToNext, -roundnessAfter).add(offsetPoint) : offsetPoint;
const startPoint = tangentToPrev && roundnessBefore > 0 ? import_editor.Vec.Mul(tangentToPrev, roundnessBefore).add(offsetPoint) : offsetPoint;
if (endPoint === offsetPoint || startPoint === offsetPoint) {
switch (command.type) {
case "move":
parts.push("M", (0, import_editor.toDomPrecision)(endPoint.x), (0, import_editor.toDomPrecision)(endPoint.y));
break;
case "line":
parts.push("L", (0, import_editor.toDomPrecision)(endPoint.x), (0, import_editor.toDomPrecision)(endPoint.y));
break;
case "cubic": {
const offsetCp1 = import_editor.Vec.Add(command.cp1, offset);
const offsetCp2 = import_editor.Vec.Add(command.cp2, offset);
parts.push(
"C",
(0, import_editor.toDomPrecision)(offsetCp1.x),
(0, import_editor.toDomPrecision)(offsetCp1.y),
(0, import_editor.toDomPrecision)(offsetCp2.x),
(0, import_editor.toDomPrecision)(offsetCp2.y),
(0, import_editor.toDomPrecision)(endPoint.x),
(0, import_editor.toDomPrecision)(endPoint.y)
);
break;
}
default:
(0, import_editor.exhaustiveSwitchError)(command, "type");
}
} else {
switch (command.type) {
case "move":
parts.push("M", (0, import_editor.toDomPrecision)(endPoint.x), (0, import_editor.toDomPrecision)(endPoint.y));
break;
case "line":
parts.push(
"L",
(0, import_editor.toDomPrecision)(startPoint.x),
(0, import_editor.toDomPrecision)(startPoint.y),
"Q",
(0, import_editor.toDomPrecision)(offsetPoint.x),
(0, import_editor.toDomPrecision)(offsetPoint.y),
(0, import_editor.toDomPrecision)(endPoint.x),
(0, import_editor.toDomPrecision)(endPoint.y)
);
break;
case "cubic": {
const offsetCp1 = import_editor.Vec.Add(command.cp1, offset);
const offsetCp2 = import_editor.Vec.Add(command.cp2, offset);
parts.push(
"C",
(0, import_editor.toDomPrecision)(offsetCp1.x),
(0, import_editor.toDomPrecision)(offsetCp1.y),
(0, import_editor.toDomPrecision)(offsetCp2.x),
(0, import_editor.toDomPrecision)(offsetCp2.y),
(0, import_editor.toDomPrecision)(offsetPoint.x),
(0, import_editor.toDomPrecision)(offsetPoint.y)
);
break;
}
default:
(0, import_editor.exhaustiveSwitchError)(command, "type");
}
}
}
}
return parts.join(" ");
}
calculateSegmentLength(lastPoint, command) {
switch (command.type) {
case "move":
return 0;
case "line":
return import_editor.Vec.Dist(lastPoint, command);
case "cubic":
return CubicBezier.length(
lastPoint.x,
lastPoint.y,
command.cp1.x,
command.cp1.y,
command.cp2.x,
command.cp2.y,
command.x,
command.y
);
default:
(0, import_editor.exhaustiveSwitchError)(command, "type");
}
}
/** @internal */
getCommands() {
return this.commands;
}
/** @internal */
getCommandInfo() {
const commandInfo = [];
for (let i = 1; i < this.commands.length; i++) {
const previous = this.commands[i - 1];
const current = this.commands[i];
if (current._info) {
commandInfo[i] = current._info;
continue;
}
if (current.type === "move") {
continue;
}
let tangentStart, tangentEnd;
switch (current.type) {
case "line":
tangentStart = tangentEnd = import_editor.Vec.Sub(previous, current).uni();
break;
case "cubic": {
tangentStart = import_editor.Vec.Sub(current.cp1, previous).uni();
tangentEnd = import_editor.Vec.Sub(current.cp2, current).uni();
break;
}
default:
(0, import_editor.exhaustiveSwitchError)(current, "type");
}
current._info = {
tangentStart,
tangentEnd,
length: this.calculateSegmentLength(previous, current)
};
commandInfo[i] = current._info;
}
return commandInfo;
}
}
const commandsSupportingRoundness = {
line: true,
move: true,
cubic: false
};
class PathBuilderGeometry2d extends import_editor.Geometry2d {
constructor(path, startIdx, endIdx, options) {
super(options);
this.path = path;
this.startIdx = startIdx;
this.endIdx = endIdx;
}
_segments = null;
getSegments() {
if (this._segments) return this._segments;
this._segments = [];
let last = this.path.commands[this.startIdx];
(0, import_editor.assert)(last.type === "move");
for (let i = this.startIdx + 1; i < this.endIdx; i++) {
const command = this.path.commands[i];
(0, import_editor.assert)(command.type !== "move");
switch (command.type) {
case "line":
this._segments.push(new import_editor.Edge2d({ start: import_editor.Vec.From(last), end: import_editor.Vec.From(command) }));
break;
case "cubic": {
this._segments.push(
new import_editor.CubicBezier2d({
start: import_editor.Vec.From(last),
cp1: import_editor.Vec.From(command.cp1),
cp2: import_editor.Vec.From(command.cp2),
end: import_editor.Vec.From(command),
resolution: command.resolution
})
);
break;
}
default:
(0, import_editor.exhaustiveSwitchError)(command, "type");
}
last = command;
}
return this._segments;
}
getVertices(filters) {
const vs = this.getSegments().flatMap((s) => s.getVertices(filters)).filter((vertex, i, vertices) => {
const prev = vertices[i - 1];
if (!prev) return true;
return !import_editor.Vec.Equals(prev, vertex);
});
if (this.isClosed) {
const last = vs[vs.length - 1];
const first = vs[0];
if (!import_editor.Vec.Equals(last, first)) {
vs.push(first);
}
}
return vs;
}
nearestPoint(point, _filters) {
let nearest = null;
let nearestDistance = Infinity;
for (const segment of this.getSegments()) {
const candidate = segment.nearestPoint(point);
const distance = import_editor.Vec.Dist2(point, candidate);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = candidate;
}
}
(0, import_editor.assert)(nearest, "No nearest point found");
return nearest;
}
hitTestLineSegment(A, B, distance = 0, filters) {
return super.hitTestLineSegment(A, B, distance, filters);
}
getSvgPathData() {
return this.path.toD({ startIdx: this.startIdx, endIdx: this.endIdx });
}
}
/*!
* Adapted from https://github.com/adobe-webplatform/Snap.svg/tree/master
* Apache License: https://github.com/adobe-webplatform/Snap.svg/blob/master/LICENSE
* https://github.com/adobe-webplatform/Snap.svg/blob/c8e483c9694517e24b282f8f59f985629f4994ce/dist/snap.svg.js#L5786
*/
const CubicBezier = {
base3(t, p1, p2, p3, p4) {
const t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4;
const t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
return t * t2 - 3 * p1 + 3 * p2;
},
/**
* Calculate the approximate length of a cubic bezier curve from (x1, y1) to (x4, y4) with
* control points (x2, y2) and (x3, y3).
*/
length(x1, y1, x2, y2, x3, y3, x4, y4, z = 1) {
z = z > 1 ? 1 : z < 0 ? 0 : z;
const z2 = z / 2;
const n = 12;
let sum = 0;
sum = 0;
for (let i = 0; i < n; i++) {
const ct = z2 * CubicBezier.Tvalues[i] + z2;
const xbase = CubicBezier.base3(ct, x1, x2, x3, x4);
const ybase = CubicBezier.base3(ct, y1, y2, y3, y4);
const comb = xbase * xbase + ybase * ybase;
sum += CubicBezier.Cvalues[i] * Math.sqrt(comb);
}
return z2 * sum;
},
Tvalues: [
-0.1252,
0.1252,
-0.3678,
0.3678,
-0.5873,
0.5873,
-0.7699,
0.7699,
-0.9041,
0.9041,
-0.9816,
0.9816
],
Cvalues: [
0.2491,
0.2491,
0.2335,
0.2335,
0.2032,
0.2032,
0.1601,
0.1601,
0.1069,
0.1069,
0.0472,
0.0472
]
};
//# sourceMappingURL=PathBuilder.js.map