Files
openmaic/OpenMAIC/lib/export/svg-path-parser.ts
2026-08-16 14:58:47 +08:00

151 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { SVGPathData } from 'svg-pathdata';
import arcToBezier from 'svg-arc-to-cubic-bezier';
import { createLogger } from '@/lib/logger';
const log = createLogger('SvgPathParser');
const typeMap = {
1: 'Z',
2: 'M',
4: 'H',
8: 'V',
16: 'L',
32: 'C',
64: 'S',
128: 'Q',
256: 'T',
512: 'A',
};
/**
* 简单解析SVG路径
* @param d SVG path d属性
*/
export const parseSvgPath = (d: string) => {
const pathData = new SVGPathData(d);
const ret = pathData.commands.map((item) => {
return { ...item, type: typeMap[item.type] };
});
return ret;
};
export type SvgPath = ReturnType<typeof parseSvgPath>;
/**
* 解析SVG路径并将圆弧A类型的路径转为三次贝塞尔C类型的路径
* @param d SVG path d属性
*
* Returns an empty array if the path is malformed (e.g. unrecognised commands).
* Mirrors the defensive behaviour of {@link getSvgPathRange}: a single bad path
* (often produced by upstream LLM hallucinations) shouldn't take down the whole
* PPTX export.
*/
export const toPoints = (d: string) => {
let pathData: SVGPathData;
try {
pathData = new SVGPathData(d);
} catch (err) {
log.warn(`Failed to parse SVG path "${d}":`, err);
return [];
}
const points = [];
for (const item of pathData.commands) {
const type = typeMap[item.type];
if (item.type === 2 || item.type === 16) {
points.push({
x: item.x,
y: item.y,
relative: item.relative,
type,
});
}
if (item.type === 32) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'cubic',
x1: item.x1,
y1: item.y1,
x2: item.x2,
y2: item.y2,
},
relative: item.relative,
type,
});
} else if (item.type === 128) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'quadratic',
x1: item.x1,
y1: item.y1,
},
relative: item.relative,
type,
});
} else if (item.type === 512) {
const lastPoint = points[points.length - 1];
// An arc may appear before any anchor point (e.g. a path that starts with
// "A", or one whose leading commands push no point). Without `lastPoint`
// there is nothing to arc from, so skip it instead of throwing — this keeps
// the documented "malformed path returns []" contract.
if (!lastPoint || !['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue;
const cubicBezierPoints = arcToBezier({
px: lastPoint.x as number,
py: lastPoint.y as number,
cx: item.x,
cy: item.y,
rx: item.rX,
ry: item.rY,
xAxisRotation: item.xRot,
largeArcFlag: item.lArcFlag,
sweepFlag: item.sweepFlag,
});
for (const cbPoint of cubicBezierPoints) {
points.push({
x: cbPoint.x,
y: cbPoint.y,
curve: {
type: 'cubic',
x1: cbPoint.x1,
y1: cbPoint.y1,
x2: cbPoint.x2,
y2: cbPoint.y2,
},
relative: false,
type: 'C',
});
}
} else if (item.type === 1) {
points.push({ close: true, type });
} else continue;
}
return points;
};
const ZERO_RANGE = { minX: 0, minY: 0, maxX: 0, maxY: 0 } as const;
export const getSvgPathRange = (path: string) => {
try {
// Delegate to svg-pathdata's bounds, which correctly account for relative
// commands, H/V/Z (which have no x/y), and arc bulge. The previous hand-rolled
// version defaulted missing coordinates to 0 — injecting a spurious (0,0) for
// Z, treating relative deltas as absolute, and ignoring arc extent.
const { minX, minY, maxX, maxY } = new SVGPathData(path).getBounds();
if ([minX, minY, maxX, maxY].some((v) => v == null || !Number.isFinite(v))) {
return { ...ZERO_RANGE };
}
return { minX, minY, maxX, maxY };
} catch {
return { ...ZERO_RANGE };
}
};
export type SvgPoints = ReturnType<typeof toPoints>;