UNPKG

svg-pathdata

Version:

Manipulate SVG path data (path[d] attribute content) simply and efficiently.

147 lines (128 loc) 4.38 kB
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; }