svgo-fix-path
Version:
Fix SVG path direction using SVGO
171 lines (166 loc) • 4.34 kB
JavaScript
'use strict';
const svgPathCommander = require('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 = svgPathCommander.getTotalLength(path);
const inc = Math.ceil(length / precission);
for (let d = 0; d < length; d += inc)
points.push(svgPathCommander.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 = svgPathCommander.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 = svgPathCommander.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 = svgPathCommander.normalizePath(d);
const paths = svgPathCommander.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 svgPathCommander.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;
});
}
}
}
};
}
};
exports.FixPathPlugin = FixPathPlugin;
exports.PathTree = PathTree;
exports.RemoveClipPathPlugin = RemoveClipPathPlugin;
exports.fixPath = fixPath;