svg-pathdata
Version:
Manipulate SVG path data (path[d] attribute content) simply and efficiently.
147 lines (128 loc) • 4.38 kB
text/typescript
import { SVGPathData } from '../index.js';
import { SVGPathDataTransformer } from '../SVGPathDataTransformer.js';
import type { SVGCommand } from '../types.js';
type SVGCommandXY = SVGCommand & { x: number; y: number; relative: boolean };
/**
* Reverses the order of path commands to go from end to start
* IMPORTANT: This function expects absolute commands as input.
* It doesn't convert relative to absolute - use SVGPathDataTransformer.TO_ABS() first if needed.
* @param commands SVG path commands in absolute form to reverse
* @param preserveSubpathOrder If true, keeps subpaths in their original order
* @returns New SVG commands in reverse order with absolute coordinates
*/
export function REVERSE_PATH(
commands: SVGCommand[],
preserveSubpathOrder = true,
): SVGCommand[] {
if (commands.length < 2) return commands;
// Extract absolute points using the transformer to track current position
const normalized = SVGPathDataTransformer.INFO((command, px, py) => ({
...command,
x: command.x ?? px,
y: command.y ?? py,
relative: command.relative ?? false,
}));
const result: SVGCommand[] = [];
let processing: SVGCommandXY[] = [];
for (const original of commands) {
const cmd: SVGCommandXY = normalized(original);
// Start a new subpath if needed
if (cmd.type === SVGPathData.MOVE_TO && processing.length > 0) {
if (preserveSubpathOrder) {
result.push(...reverseSubpath(processing));
} else {
result.unshift(...reverseSubpath(processing));
}
processing = []; // Clear the current subpath
}
processing.push(cmd);
}
if (processing.length > 0) {
if (preserveSubpathOrder) {
result.push(...reverseSubpath(processing));
} else {
result.unshift(...reverseSubpath(processing));
}
}
// Join the reversed subpaths in original order
return result;
}
function reverseSubpath(commands: SVGCommandXY[]): SVGCommand[] {
// Check if path is explicitly closed (ends with CLOSE_PATH)
const isExplicitlyClosed =
commands[commands.length - 1]?.type === SVGPathData.CLOSE_PATH;
// Start with a move to the last explicit point
// (if path ends with Z, use the point before Z)
const startPointIndex = isExplicitlyClosed
? commands.length - 2
: commands.length - 1;
const reversed: SVGCommand[] = [
{
type: SVGPathData.MOVE_TO,
relative: false,
x: commands[startPointIndex].x,
y: commands[startPointIndex].y,
},
];
// Process each segment in reverse order
for (let i = startPointIndex; i > 0; i--) {
const curCmd = commands[i];
const prevPoint = commands[i - 1];
if (curCmd.relative) {
throw new Error(
'Relative command are not supported convert first with `toAbs()`',
);
}
// Handle the current command type
switch (curCmd.type) {
case SVGPathData.HORIZ_LINE_TO:
reversed.push({
type: SVGPathData.HORIZ_LINE_TO,
relative: false,
x: prevPoint.x,
});
break;
case SVGPathData.VERT_LINE_TO:
reversed.push({
type: SVGPathData.VERT_LINE_TO,
relative: false,
y: prevPoint.y,
});
break;
case SVGPathData.LINE_TO:
case SVGPathData.MOVE_TO:
reversed.push({
type: SVGPathData.LINE_TO,
relative: false,
x: prevPoint.x,
y: prevPoint.y,
});
break;
case SVGPathData.CURVE_TO:
reversed.push({
type: SVGPathData.CURVE_TO,
relative: false,
x: prevPoint.x,
y: prevPoint.y,
x1: curCmd.x2,
y1: curCmd.y2,
x2: curCmd.x1,
y2: curCmd.y1,
});
break;
case SVGPathData.SMOOTH_CURVE_TO:
throw new Error(`Unsupported command: S (smooth cubic bezier)`);
case SVGPathData.SMOOTH_QUAD_TO:
throw new Error(`Unsupported command: T (smooth quadratic bezier)`);
case SVGPathData.ARC:
throw new Error(`Unsupported command: A (arc)`);
case SVGPathData.QUAD_TO:
throw new Error(`Unsupported command: Q (quadratic bezier)`);
}
}
// If the original path was explicitly closed, preserve the Z command
if (isExplicitlyClosed) {
reversed.push({ type: SVGPathData.CLOSE_PATH });
}
return reversed;
}