svgo-fix-path
Version:
Fix SVG path direction using SVGO
166 lines (162 loc) • 4.21 kB
JavaScript
import { getPathBBox, reversePath, normalizePath, splitPath, pathToString, getTotalLength, getPointAtLength } from 'svg-path-commander';
function isInside(inner, outer, outerBBox) {
return inner.every((point) => {
return isPoinInBBox(point, outerBBox) && isPointInPoly(point, outer);
});
}
function isPoinInBBox(point, bbox) {
return point.x >= bbox.x && point.x <= bbox.x2 && (point.y >= bbox.y && point.y <= bbox.y2);
}
function isPointInPoly(point, polygon) {
let c = false;
for (let i = -1, l = polygon.length, j = l - 1; ++i < l; j = i) {
if ((polygon[i].y <= point.y && point.y < polygon[j].y || polygon[j].y <= point.y && point.y < polygon[i].y) && point.x < (polygon[j].x - polygon[i].x) * (point.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x)
c = !c;
}
return c;
}
function getPolygon(path, precission = 30) {
const points = [];
const length = getTotalLength(path);
const inc = Math.ceil(length / precission);
for (let d = 0; d < length; d += inc)
points.push(getPointAtLength(path, d));
return points;
}
function getArea(points) {
const n = points.length;
let area = 0;
for (let i = 0; i < n; i++) {
const p1 = points[i];
const p2 = points[(i + 1) % n];
area += p1.x * p2.y - p2.x * p1.y;
}
return area;
}
class PathTree {
path;
area;
polygon;
bbox;
parent;
children;
constructor(path) {
this.children = /* @__PURE__ */ new Set();
this.value = path;
}
set value(path) {
if (path) {
const polygon = getPolygon(path);
const area = getArea(polygon);
const bbox = getPathBBox(path);
this.path = path;
this.polygon = polygon;
this.area = area;
this.bbox = bbox;
}
}
get value() {
return this.path;
}
get dir() {
if (this.area !== void 0)
return this.area >= 0 ? "cw" : "ccw";
}
addChild(target) {
for (const child of this.children) {
const c = child.compare(target);
if (c === -1) {
child.setParent(target);
return this;
}
if (c === 1) {
child.addChild(target);
return this;
}
}
this.children.add(target);
target.parent = this;
return this;
}
setParent(parent) {
const grandParent = this.parent;
if (grandParent) {
grandParent.children.delete(this);
grandParent.addChild(parent);
}
parent.addChild(this);
return this;
}
compare(other) {
if (this.polygon && other.polygon && this.bbox && other.bbox) {
if (isInside(other.polygon, this.polygon, this.bbox))
return 1;
if (isInside(this.polygon, other.polygon, other.bbox))
return -1;
}
return 0;
}
reverse() {
if (this.value)
this.value = reversePath(this.value);
return this;
}
static from(paths) {
const root = new PathTree();
for (const path of paths)
root.addChild(new PathTree(path));
return root;
}
}
function fixPath(d) {
const path = normalizePath(d);
const paths = splitPath(path);
const root = PathTree.from(paths);
const nodes = [...root.children];
const result = [];
while (nodes.length > 0) {
const node = nodes.shift();
if (node) {
if (node.value) {
const pDir = node.parent?.dir ?? "ccw";
const dir = node.dir;
if (pDir === dir)
node.reverse();
result.push(node.value);
}
if (node.children.size > 0)
nodes.push(...node.children);
}
}
return pathToString(result.flat());
}
const FixPathPlugin = {
name: "fix-path",
fn: () => {
return {
element: {
enter(node) {
if (node.name === "path" && node.attributes.d)
node.attributes.d = fixPath(node.attributes.d);
}
}
};
}
};
const RemoveClipPathPlugin = {
name: "remove-clip-path",
fn: () => {
return {
element: {
exit(node, parentNode) {
if (node.name === "g" && node.attributes["clip-path"]) {
parentNode.children = node.children.map((child) => {
return child;
});
}
}
}
};
}
};
export { FixPathPlugin, PathTree, RemoveClipPathPlugin, fixPath };